From d4bb4dc0a36efbd59821baaaec783ea4b9e5787a Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Wed, 31 Jul 2024 17:32:51 -0700 Subject: [PATCH 01/10] unfinished --- .../odsp-client/src/interfaces.ts | 5 - .../odsp-client/src/odspClient.ts | 121 +++++++++++++----- 2 files changed, 89 insertions(+), 37 deletions(-) diff --git a/packages/service-clients/odsp-client/src/interfaces.ts b/packages/service-clients/odsp-client/src/interfaces.ts index 5b251ffb0301..5b1a8bf5bb1f 100644 --- a/packages/service-clients/odsp-client/src/interfaces.ts +++ b/packages/service-clients/odsp-client/src/interfaces.ts @@ -32,11 +32,6 @@ export interface OdspConnectionConfig { * SharePoint Embedded Container Id of the tenant where Fluid containers are created */ 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. - */ - filePath: string; } /** * @beta diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index 26c6ae56da21..bc2f9a219e86 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -89,28 +89,92 @@ function wrapConfigProvider(baseConfigProvider?: IConfigProviderBase): IConfigPr return wrapConfigProviderWithDefaults(baseConfigProvider, odspClientFeatureGates); } +/** + * Creates OdspClient + * @param properties - properties + * @returns OdspClient + */ +export function createOdspClientCore( + driverFactory: IDocumentServiceFactory, + urlResolver: OdspDriverUrlResolver, + logger?: ITelemetryBaseLogger, + configProvider?: IConfigProviderBase, +): OdspClient { + return new OdspClient(driverFactory, urlResolver, logger, configProvider); +} + +/** + * Creates OdspClient + * @param properties - properties + * @returns OdspClient + */ +export function createOdspClient( + properties: OdspClientProps, +): OdspClient { + return createOdspClientCore( + new OdspDocumentServiceFactory( + async (options) => getStorageToken(options, properties.connection.tokenProvider), + async (options) => getWebsocketToken(options, properties.connection.tokenProvider), + ), + new OdspDriverUrlResolver(), + 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; +export class OdspClient extends OdspClientCore { + public constructor( + documentServiceFactory: IDocumentServiceFactory, + urlResolver: OdspDriverUrlResolver, + logger?: ITelemetryBaseLogger, + configProvider?: IConfigProviderBase, + ) { + super( + documentServiceFactory, + urlResolver, + logger, + configProvider, + ); + } + + public override async getContainer( + id: string, + containerSchema: T, + ): Promise<{ + container: IFluidContainer; + services: OdspContainerServices; + }> { + const url = createOdspUrl({ + siteUrl: this.connectionConfig.siteUrl, + driveId: this.connectionConfig.driveId, + itemId: id, + dataStorePath: "", + }); + return super.getContainer(url, containerSchema); + } + +} + +/** + * 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 OdspClientCore { private readonly connectionConfig: OdspConnectionConfig; - private readonly logger: ITelemetryBaseLogger | undefined; - public constructor(properties: OdspClientProps) { + public constructor( + private readonly documentServiceFactory: IDocumentServiceFactory, + private readonly urlResolver: OdspDriverUrlResolver, + private readonly logger?: ITelemetryBaseLogger, + private readonly configProvider?: IConfigProviderBase, + ) { 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), - ); - - this.urlResolver = new OdspDriverUrlResolver(); - this.configProvider = wrapConfigProvider(properties.configProvider); } public async createContainer( @@ -126,7 +190,10 @@ export class OdspClient { config: {}, }); - const fluidContainer = await this.createFluidContainer(container, this.connectionConfig); + const rootDataObject = await this.getContainerEntryPoint(container); + const fluidContainer = createFluidContainer({ container, rootDataObject }); + + this.addAttachCallback(container, fluidContainer); const services = await this.getContainerServices(container); @@ -134,19 +201,13 @@ export class OdspClient { } public async getContainer( - id: string, + url: string, containerSchema: T, ): Promise<{ container: IFluidContainer; services: OdspContainerServices; }> { const loader = this.createLoader(containerSchema); - const url = createOdspUrl({ - siteUrl: this.connectionConfig.siteUrl, - driveId: this.connectionConfig.driveId, - itemId: id, - dataStorePath: "", - }); const container = await loader.resolve({ url }); const fluidContainer = createFluidContainer({ @@ -190,21 +251,20 @@ export class OdspClient { }); } - private async createFluidContainer( + private addAttachCallback( container: IContainer, - connection: OdspConnectionConfig, - ): Promise { - const rootDataObject = await this.getContainerEntryPoint(container); + fluidContainer: IFluidContainer, + ): void { /** * See {@link FluidContainer.attach} */ - const attach = async ( + fluidContainer.attach = async ( odspProps?: ContainerAttachProps, ): Promise => { const createNewRequest: IRequest = createOdspCreateContainerRequest( - connection.siteUrl, - connection.driveId, + this.connectionConfig.siteUrl, + this.connectionConfig.driveId, odspProps?.filePath ?? "", odspProps?.fileName ?? uuid(), ); @@ -226,9 +286,6 @@ export class OdspClient { */ return resolvedUrl.itemId; }; - const fluidContainer = createFluidContainer({ container, rootDataObject }); - fluidContainer.attach = attach; - return fluidContainer; } private async getContainerServices(container: IContainer): Promise { From 35ba1f3be46359d52e2bc9938868823f97478481 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Mon, 5 Aug 2024 14:14:51 -0700 Subject: [PATCH 02/10] OdspClient --- .../shared-tree-demo/src/clientProps.ts | 4 +- .../odsp-client/shared-tree-demo/src/fluid.ts | 21 ++- .../shared-tree-demo/src/index.tsx | 9 +- .../shared-tree-demo/src/reactApp.tsx | 6 +- .../utils/bundle-size-tests/src/odspClient.ts | 4 +- ...dsp-driver-definitions.legacy.alpha.api.md | 2 +- .../odsp-driver-definitions/src/factory.ts | 1 + .../api-report/fluid-framework.beta.api.md | 7 +- .../fluid-framework.legacy.alpha.api.md | 7 +- .../fluid-framework.legacy.public.api.md | 7 +- .../api-report/fluid-framework.public.api.md | 7 +- .../framework/fluid-framework/src/index.ts | 1 - .../api-report/fluid-static.alpha.api.md | 7 +- .../api-report/fluid-static.beta.api.md | 7 +- .../api-report/fluid-static.public.api.md | 7 +- packages/framework/fluid-static/package.json | 7 +- .../fluid-static/src/fluidContainer.ts | 33 ++-- packages/framework/fluid-static/src/index.ts | 1 - .../validateFluidStaticPrevious.generated.ts | 6 +- packages/framework/fluid-static/src/types.ts | 6 - .../azure-client/src/AzureClient.ts | 6 +- .../odsp-client/src/test/OdspClientFactory.ts | 13 +- .../odsp-client/src/test/audience.spec.ts | 17 +- .../src/test/containerCreate.spec.ts | 20 +- .../odsp-client/src/test/ddsTests.spec.ts | 29 +-- .../service-clients/odsp-client/README.md | 4 +- .../api-report/odsp-client.alpha.api.md | 75 +++++--- .../api-report/odsp-client.beta.api.md | 42 +---- .../service-clients/odsp-client/src/index.ts | 11 +- .../odsp-client/src/interfaces.ts | 108 +++++++++-- .../odsp-client/src/odspClient.ts | 177 ++++++++++-------- .../odsp-client/src/test/odspClient.spec.ts | 11 +- .../src/TinyliciousClient.ts | 4 +- 33 files changed, 390 insertions(+), 277 deletions(-) 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..b722da338195 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, + IOdspFluidContainer, + // eslint-disable-next-line import/no-internal-modules +} from "@fluidframework/odsp-client/internal"; +import { ContainerSchema, SharedTree } 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. @@ -21,13 +26,13 @@ export const loadFluidData = async ( schema: ContainerSchema, ): Promise<{ services: OdspContainerServices; - container: IFluidContainer; + container: IOdspFluidContainer; }> => { const { container, services, - }: { container: IFluidContainer; services: OdspContainerServices } = - await client.getContainer(itemId, schema); + }: { container: IOdspFluidContainer; services: OdspContainerServices } = + await client.getContainer({ itemId }, schema); return { services, container }; }; @@ -36,14 +41,14 @@ export const createFluidData = async ( schema: ContainerSchema, ): Promise<{ services: OdspContainerServices; - container: IFluidContainer; + container: IOdspFluidContainer; }> => { // 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 } = + }: { container: IOdspFluidContainer; services: OdspContainerServices } = await client.createContainer(schema); return { services, container }; 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..ca0f47507aca 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,9 @@ * Licensed under the MIT License. */ -import { IFluidContainer, ITree } from "fluid-framework"; +// eslint-disable-next-line import/no-internal-modules +import { IOdspFluidContainer } from "@fluidframework/odsp-client/internal"; +import { ITree } from "fluid-framework"; import React from "react"; import ReactDOM from "react-dom"; @@ -23,7 +25,7 @@ async function start(): Promise { // a new container. let itemId: string = location.hash.slice(1); const createNew = itemId.length === 0; - let container: IFluidContainer; + let container: IOdspFluidContainer; if (createNew) { ({ container } = await createFluidData(containerSchema)); @@ -95,7 +97,8 @@ 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" }); + const res = await container.attach({ 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/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx b/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx index 4279201403d2..a064d8728771 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx +++ b/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx @@ -5,7 +5,9 @@ /* eslint-disable prefer-template */ -import { IFluidContainer, Tree, TreeView } from "fluid-framework"; +// eslint-disable-next-line import/no-internal-modules +import { IOdspFluidContainer } from "@fluidframework/odsp-client/internal"; +import { Tree, TreeView } from "fluid-framework"; import React, { ReactNode, useEffect, useState } from "react"; import { App, Letter } from "./schema.js"; @@ -128,7 +130,7 @@ function TopRow(props: { app: App }): JSX.Element { export function ReactApp(props: { data: TreeView; - container: IFluidContainer; + container: IOdspFluidContainer; cellSize: { x: number; y: number }; canvasSize: { x: number; y: number }; }): JSX.Element { 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..917e154c7b3e 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; 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/framework/fluid-framework/api-report/fluid-framework.beta.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md index 699ce0f5bdbe..876bf5b13cd9 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[]; @@ -332,8 +329,8 @@ export type IEventTransformer = TEvent extends { } ? TransformedEvent : TransformedEvent; // @public @sealed -export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; +export interface IFluidContainer Promise> extends IEventProvider { + attach: TAttachType; 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..df15297ec69a 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[]; @@ -370,8 +367,8 @@ export type IEventTransformer = TEvent extends { } ? TransformedEvent : TransformedEvent; // @public @sealed -export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; +export interface IFluidContainer Promise> extends IEventProvider { + attach: TAttachType; 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..3dee853c4eb8 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[]; @@ -360,8 +357,8 @@ export type IEventTransformer = TEvent extends { } ? TransformedEvent : TransformedEvent; // @public @sealed -export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; +export interface IFluidContainer Promise> extends IEventProvider { + attach: TAttachType; 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..8cbafd7f170c 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[]; @@ -332,8 +329,8 @@ export type IEventTransformer = TEvent extends { } ? TransformedEvent : TransformedEvent; // @public @sealed -export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; +export interface IFluidContainer Promise> extends IEventProvider { + attach: TAttachType; 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..deb6e50f18f3 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[]; @@ -23,8 +20,8 @@ export interface IConnection { } // @public @sealed -export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; +export interface IFluidContainer Promise> extends IEventProvider { + attach: TAttachType; 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..a485121ba637 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[]; @@ -23,8 +20,8 @@ export interface IConnection { } // @public @sealed -export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; +export interface IFluidContainer Promise> extends IEventProvider { + attach: TAttachType; 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..9d570e6eec5e 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[]; @@ -23,8 +20,8 @@ export interface IConnection { } // @public @sealed -export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; +export interface IFluidContainer Promise> extends IEventProvider { + attach: TAttachType; 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..4b6a219fb5a0 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. @@ -106,8 +106,10 @@ export interface IFluidContainerEvents extends IEvent { * @sealed * @public */ -export interface IFluidContainer - extends IEventProvider { +export interface IFluidContainer< + TContainerSchema extends ContainerSchema = ContainerSchema, + TAttachType = () => Promise, +> extends IEventProvider { /** * Provides the current connected state of the container */ @@ -170,7 +172,7 @@ export interface IFluidContainer; + attach: TAttachType; /** * Attempts to connect the container to the delta stream and process operations. @@ -236,12 +238,17 @@ export interface IFluidContainer unknown, >(props: { container: IContainer; rootDataObject: IRootDataObject; -}): IFluidContainer { - return new FluidContainer(props.container, props.rootDataObject); +}): IFluidContainer { + return new FluidContainer( + props.container, + props.rootDataObject, + ); } /** @@ -253,9 +260,13 @@ export function createFluidContainer< * Note: this implementation is not complete. Consumers who rely on {@link IFluidContainer.attach} * will need to utilize or provide a service-specific implementation of this type that implements that method. */ -class FluidContainer +class FluidContainer< + TContainerSchema extends ContainerSchema, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TAttachType extends (...args: any[]) => unknown, + > extends TypedEventEmitter - implements IFluidContainer + implements IFluidContainer { private readonly connectedHandler = (): boolean => this.emit("connected"); private readonly disconnectedHandler = (): boolean => this.emit("disconnected"); @@ -323,12 +334,12 @@ 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 readonly attach = ((...args: Parameters): ReturnType => { if (this.container.attachState !== AttachState.Detached) { throw new Error("Cannot attach container. Container is not in detached state."); } throw new Error("Cannot attach container. Attach method not provided."); - } + }) as TAttachType; /** * {@inheritDoc IFluidContainer.connect} 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/azure-client/src/AzureClient.ts b/packages/service-clients/azure-client/src/AzureClient.ts index 4a61a56b4855..09fd347806b7 100644 --- a/packages/service-clients/azure-client/src/AzureClient.ts +++ b/packages/service-clients/azure-client/src/AzureClient.ts @@ -174,7 +174,7 @@ export class AzureClient { url.searchParams.append("containerId", encodeURIComponent(id)); const container = await loader.resolve({ url: url.href }); const rootDataObject = await this.getContainerEntryPoint(container); - const fluidContainer = createFluidContainer({ + const fluidContainer = createFluidContainer Promise>({ container, rootDataObject, }); @@ -213,7 +213,7 @@ export class AzureClient { headers: { [LoaderHeader.version]: version.id }, }); const rootDataObject = await this.getContainerEntryPoint(container); - const fluidContainer = createFluidContainer({ + const fluidContainer = createFluidContainer Promise>({ container, rootDataObject, }); @@ -322,7 +322,7 @@ export class AzureClient { } return container.resolvedUrl.id; }; - const fluidContainer = createFluidContainer({ + const fluidContainer = createFluidContainer Promise>({ container, rootDataObject, }); 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..cccc7f7e9bca 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(); @@ -46,7 +46,8 @@ describe("Fluid audience", () => { */ it("can find original member", async () => { const { container, services } = await client.createContainer(schema); - const itemId = await container.attach(); + const res = await container.attach(); + const itemId = res.itemId; if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -80,7 +81,8 @@ describe("Fluid audience", () => { */ it.skip("can find partner member", async () => { const { container, services } = await client.createContainer(schema); - const itemId = await container.attach(); + const res = await container.attach(); + const itemId = res.itemId; if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -108,7 +110,7 @@ describe("Fluid audience", () => { "Fluid.Container.ForceWriteConnection": true, }), ); - const { services: servicesGet } = await client2.getContainer(itemId, schema); + const { services: servicesGet } = await client2.getContainer({ itemId }, schema); /* This is a workaround for a known bug, we should have one member (self) upon container connection */ const partner = await waitForMember(servicesGet.audience, client2Creds.email); @@ -134,7 +136,8 @@ describe("Fluid audience", () => { */ it.skip("can observe member leaving", async () => { const { container } = await client.createContainer(schema); - const itemId = await container.attach(); + const res = await container.attach(); + const itemId = res.itemId; if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -151,7 +154,7 @@ describe("Fluid audience", () => { "Fluid.Container.ForceWriteConnection": true, }), ); - const { services: servicesGet } = await client2.getContainer(itemId, schema); + const { services: servicesGet } = await client2.getContainer({ itemId }, schema); /* This is a workaround for a known bug, we should have one member (self) upon container connection */ const partner = await waitForMember(servicesGet.audience, client2Creds.email); 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..516fd1c18768 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(); @@ -50,7 +50,8 @@ describe("Container create scenarios", () => { ); // Make sure we can attach. - const itemId = await container.attach(); + const res = await container.attach(); + const itemId = res.itemId; assert.strictEqual(typeof itemId, "string", "Attach did not return a string ID"); }); @@ -62,7 +63,8 @@ describe("Container create scenarios", () => { */ it("can attach a container", async () => { const { container } = await client.createContainer(schema); - const itemId = await container.attach(); + const res = await container.attach(); + const itemId = res.itemId; if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -87,7 +89,8 @@ describe("Container create scenarios", () => { */ it("cannot attach a container twice", async () => { const { container } = await client.createContainer(schema); - const itemId = await container.attach(); + const res = await container.attach(); + const itemId = res.itemId; if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -113,7 +116,8 @@ describe("Container create scenarios", () => { */ it("can retrieve existing ODSP container successfully", async () => { const { container: newContainer } = await client.createContainer(schema); - const itemId = await newContainer.attach(); + const res = await newContainer.attach(); + const itemId = res.itemId; if (newContainer.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => newContainer.once("connected", () => resolve()), { @@ -122,7 +126,7 @@ describe("Container create scenarios", () => { }); } - const resources = client.getContainer(itemId, schema); + const resources = client.getContainer({ itemId }, schema); await assert.doesNotReject( resources, () => true, @@ -136,7 +140,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({ itemId: "containerConfig" }, 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..c802a07ebf38 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, @@ -44,7 +44,8 @@ describe("Fluid data updates", () => { */ it("can set DDSes as initial objects for a container", async () => { const { container: newContainer } = await client.createContainer(schema); - const itemId = await newContainer.attach(); + const res = await newContainer.attach(); + const itemId = res.itemId; if (newContainer.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => newContainer.once("connected", () => resolve()), { @@ -53,7 +54,7 @@ describe("Fluid data updates", () => { }); } - const resources = client.getContainer(itemId, schema); + const resources = client.getContainer({ itemId }, schema); await assert.doesNotReject( resources, () => true, @@ -75,7 +76,8 @@ describe("Fluid data updates", () => { */ it("can change DDSes within initialObjects value", async () => { const { container } = await client.createContainer(schema); - const itemId = await container.attach(); + const res = await container.attach(); + const itemId = res.itemId; if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -89,7 +91,7 @@ describe("Fluid data updates", () => { map1Create.set("new-key", "new-value"); const valueCreate: string | undefined = map1Create.get("new-key"); - const { container: containerGet } = await client.getContainer(itemId, schema); + const { container: containerGet } = await client.getContainer({ itemId }, schema); const map1Get = containerGet.initialObjects.map1; const valueGet: string | undefined = await mapWait(map1Get, "new-key"); assert.strictEqual(valueGet, valueCreate, "container can't change initial objects"); @@ -108,7 +110,8 @@ describe("Fluid data updates", () => { }, }; const { container } = await client.createContainer(doSchema); - const itemId = await container.attach(); + const res = await container.attach(); + const itemId = res.itemId; if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -127,7 +130,7 @@ describe("Fluid data updates", () => { "container returns the wrong type for mdo2", ); - const { container: containerGet } = await client.getContainer(itemId, doSchema); + const { container: containerGet } = await client.getContainer({ itemId }, doSchema); const initialObjectsGet = containerGet.initialObjects; assert( initialObjectsGet.mdo1 instanceof TestDataObject, @@ -155,7 +158,8 @@ describe("Fluid data updates", () => { }, }; const { container } = await client.createContainer(doSchema); - const itemId = await container.attach(); + const res = await container.attach(); + const itemId = res.itemId; if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -178,7 +182,7 @@ describe("Fluid data updates", () => { "container returns the wrong type for mdo3", ); - const { container: containerGet } = await client.getContainer(itemId, doSchema); + const { container: containerGet } = await client.getContainer({ itemId }, doSchema); const initialObjectsGet = containerGet.initialObjects; assert( initialObjectsGet.mdo1 instanceof TestDataObject, @@ -215,7 +219,8 @@ describe("Fluid data updates", () => { assert.strictEqual(mdo2.value, 3); - const itemId = await container.attach(); + const res = await container.attach(); + const itemId = res.itemId; if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -224,7 +229,7 @@ describe("Fluid data updates", () => { }); } - const { container: containerGet } = await client.getContainer(itemId, doSchema); + const { container: containerGet } = await client.getContainer({ itemId }, doSchema); const initialObjectsGet = containerGet.initialObjects; const mdo2get: CounterTestDataObject = initialObjectsGet.mdo2; diff --git a/packages/service-clients/odsp-client/README.md b/packages/service-clients/odsp-client/README.md index caeed45481ba..0a96928a7437 100644 --- a/packages/service-clients/odsp-client/README.md +++ b/packages/service-clients/odsp-client/README.md @@ -106,7 +106,9 @@ const containerSchema = { const odspClient = new OdspClient(clientProps); const { container, services } = await odspClient.createContainer(containerSchema); -const itemId = await container.attach(); +const response = await container.attach(); +const itemId = response.itemId; + ``` ## 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..9d9c5eb64294 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,57 +4,88 @@ ```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; -} +// @alpha +export type IOdspAudience = IServiceAudience; -// @beta @sealed -export class OdspClient { - constructor(properties: OdspClientProps); - // (undocumented) +// @alpha +export interface IOdspClient { createContainer(containerSchema: T): Promise<{ - container: IFluidContainer; + container: IOdspFluidContainer; services: OdspContainerServices; }>; - // (undocumented) - getContainer(id: string, containerSchema: T): Promise<{ - container: IFluidContainer; + getContainer(request: OdspGetContainerArgType, containerSchema: T): Promise<{ + container: IOdspFluidContainer; services: OdspContainerServices; }>; } -// @beta (undocumented) +// @alpha +export type IOdspFluidContainer = IFluidContainer; + +// @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; - siteUrl: string; +export interface OdspConnectionConfig extends OdspSiteIdentification { tokenProvider: IOdspTokenProvider; } -// @beta +// @alpha +export type OdspContainerAttachArgType = { + filePath?: string | undefined; + fileName?: string | undefined; + createShareLinkType?: ISharingLinkKind; +} | { + itemId: string; +}; + +// @alpha +export interface OdspContainerAttachReturnType { + itemId: string; + shareLinkInfo?: ShareLinkInfoType; +} + +// @alpha +export type OdspContainerAttachType = (param?: OdspContainerAttachArgType) => Promise; + +// @alpha export interface OdspContainerServices { audience: IOdspAudience; } -// @beta +// @alpha +export type OdspGetContainerArgType = { + itemId: string; +} | IRequest; + +// @alpha export interface OdspMember extends IMember { email: string; id: string; name: string; } +// @beta +export interface OdspSiteIdentification { + driveId: string; + siteUrl: string; +} + // @beta export interface TokenResponse { fromCache?: boolean; 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..7c0bdcb2cefe 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 @@ -4,55 +4,21 @@ ```ts -// @beta -export type IOdspAudience = IServiceAudience; - // @beta export interface IOdspTokenProvider { fetchStorageToken(siteUrl: string, refresh: boolean): Promise; 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; - siteUrl: string; +export interface OdspConnectionConfig extends OdspSiteIdentification { tokenProvider: IOdspTokenProvider; } // @beta -export interface OdspContainerServices { - audience: IOdspAudience; -} - -// @beta -export interface OdspMember extends IMember { - email: string; - id: string; - name: string; +export interface OdspSiteIdentification { + driveId: string; + siteUrl: string; } // @beta diff --git a/packages/service-clients/odsp-client/src/index.ts b/packages/service-clients/odsp-client/src/index.ts index 55c28a72e802..f0fc4467de1e 100644 --- a/packages/service-clients/odsp-client/src/index.ts +++ b/packages/service-clients/odsp-client/src/index.ts @@ -14,12 +14,21 @@ */ export type { + OdspSiteIdentification, OdspConnectionConfig, OdspClientProps, OdspContainerServices, IOdspAudience, OdspMember, TokenResponse, + OdspContainerAttachArgType, + OdspContainerAttachType, + OdspContainerAttachReturnType, + OdspGetContainerArgType, } from "./interfaces.js"; -export { OdspClient } from "./odspClient.js"; +export { + type IOdspClient, + type IOdspFluidContainer, + 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 5b1a8bf5bb1f..f73277ac8476 100644 --- a/packages/service-clients/odsp-client/src/interfaces.ts +++ b/packages/service-clients/odsp-client/src/interfaces.ts @@ -6,8 +6,15 @@ import type { IConfigProviderBase, ITelemetryBaseLogger, + IRequest, } from "@fluidframework/core-interfaces"; import type { IMember, IServiceAudience } from "@fluidframework/fluid-static"; +import type { + ISharingLinkKind, + ShareLinkInfoType, + IPersistedCache, + HostStoragePolicy, +} from "@fluidframework/odsp-driver-definitions/internal"; import type { IOdspTokenProvider } from "./token.js"; @@ -17,12 +24,7 @@ import type { IOdspTokenProvider } from "./token.js"; * required for ODSP. * @beta */ -export interface OdspConnectionConfig { - /** - * Instance that provides AAD endpoint tokens for Push and SharePoint - */ - tokenProvider: IOdspTokenProvider; - +export interface OdspSiteIdentification { /** * Site url representing ODSP resource location. It points to the specific SharePoint site where you can store and access the containers you create. */ @@ -33,9 +35,23 @@ export interface OdspConnectionConfig { */ driveId: string; } + /** + * Defines the necessary properties that will be applied to all containers + * created by an OdspClient instance. This includes callbacks for the authentication tokens + * required for ODSP. * @beta */ +export interface OdspConnectionConfig extends OdspSiteIdentification { + /** + * Instance that provides AAD endpoint tokens for Push and SharePoint + */ + tokenProvider: IOdspTokenProvider; +} + +/** + * @alpha + */ export interface OdspClientProps { /** * Configuration for establishing a connection with the ODSP Fluid Service (Push). @@ -51,22 +67,84 @@ 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 + * Argument type of IFluidContainer.attach() for containers created by IOdspClient + * Specifies location / name of the file. + * 1. If undefined, file with random name (uuid) will be created. + * 2. Specifies file path / file name to be created. If 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 + * 3. (Microsoft internal only) files supporting FF format on alternate parition could point to existing file. + * @alpha + */ +export type OdspContainerAttachArgType = + | { + /** + * The file path where Fluid containers are created. If undefined, the file is created at the root. + */ + filePath?: string | undefined; + + /** + * The file name of the Fluid file. If undefined, the file is named with a GUID. + */ + fileName?: string | undefined; + + /** + * If provided, will instrcuct Sharepoint to create a sharing link as part of file creation flow. + */ + createShareLinkType?: ISharingLinkKind; + } + | { + itemId: string; + }; + +/** + * An object type returned by IOdspFluidContainer.attach() call. * * @alpha */ -export interface OdspContainerAttachProps { +export interface OdspContainerAttachReturnType { + /** + * An ID of the document created. This ID could be passed to future IOdspClient.getContainer() call + */ + itemId: string; /** - * The file path where Fluid containers are created. If undefined, the file is created at the root. + * If OdspContainerAttachArgType.createShareLinkType was provided at the time of IOdspFluidContainer.attach() call, + * this value will contain sharing link to just created file. */ - filePath: string | undefined; + shareLinkInfo?: ShareLinkInfoType; +} + +/** + * IFluidContainer.attach() function signature for IOdspClient + * @alpha + */ +export type OdspContainerAttachType = ( + param?: OdspContainerAttachArgType, +) => Promise; + +/** + * Type of argument to IOdspClient.getContainer() + * @alpha + */ +export interface OdspGetContainerArgType { + itemId: string; /** - * The file name of the Fluid file. If undefined, the file is named with a GUID. + * 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. */ - fileName: string | undefined; + sharingLinkToRedeem?: string; } /** @@ -75,7 +153,7 @@ export interface OdspContainerAttachProps { * FluidContainer is persisted in the backend and consumed by users. Any functionality regarding * how the data is handled within the FluidContainer itself, i.e. which data objects or DDSes to * use, will not be included here but rather on the FluidContainer class itself. - * @beta + * @alpha */ export interface OdspContainerServices { /** @@ -88,7 +166,7 @@ export interface OdspContainerServices { * Since ODSP provides user names and email for all of its members, we extend the * {@link @fluidframework/fluid-static#IMember} interface to include this service-specific value. * It will be returned for all audience members connected. - * @beta + * @alpha */ export interface OdspMember extends IMember { /** @@ -107,7 +185,7 @@ export interface OdspMember extends IMember { /** * Audience object for ODSP containers - * @beta + * @alpha */ export type IOdspAudience = IServiceAudience; diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index bc2f9a219e86..2b098f7dec46 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -18,11 +18,7 @@ import { 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"; +import type { ContainerSchema, IFluidContainer } from "@fluidframework/fluid-static"; import type { IRootDataObject } from "@fluidframework/fluid-static/internal"; import { createDOProviderContainerRuntimeFactory, @@ -35,17 +31,21 @@ import { createOdspCreateContainerRequest, createOdspUrl, isOdspResolvedUrl, + SharingLinkHeader, } from "@fluidframework/odsp-driver/internal"; import type { OdspResourceTokenFetchOptions } 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, + OdspSiteIdentification, + OdspContainerAttachArgType, + OdspContainerAttachType, OdspContainerServices, + OdspContainerAttachReturnType, + OdspGetContainerArgType, } from "./interfaces.js"; import { createOdspAudienceMember } from "./odspAudience.js"; import { type IOdspTokenProvider } from "./token.js"; @@ -54,22 +54,14 @@ 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); } /** @@ -94,93 +86,94 @@ function wrapConfigProvider(baseConfigProvider?: IConfigProviderBase): IConfigPr * @param properties - properties * @returns OdspClient */ -export function createOdspClientCore( +function createOdspClientCore( driverFactory: IDocumentServiceFactory, urlResolver: OdspDriverUrlResolver, + connectionConfig: OdspSiteIdentification, logger?: ITelemetryBaseLogger, configProvider?: IConfigProviderBase, -): OdspClient { - return new OdspClient(driverFactory, urlResolver, logger, configProvider); +): IOdspClient { + return new OdspClient(driverFactory, urlResolver, connectionConfig, logger, configProvider); } /** * Creates OdspClient * @param properties - properties * @returns OdspClient + * @alpha */ -export function createOdspClient( - properties: OdspClientProps, -): OdspClient { +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, ), new OdspDriverUrlResolver(), + 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 + * Fluid Container type + * @alpha */ -export class OdspClient extends OdspClientCore { - public constructor( - documentServiceFactory: IDocumentServiceFactory, - urlResolver: OdspDriverUrlResolver, - logger?: ITelemetryBaseLogger, - configProvider?: IConfigProviderBase, - ) { - super( - documentServiceFactory, - urlResolver, - logger, - configProvider, - ); - } +export type IOdspFluidContainer = + IFluidContainer; - public override async getContainer( - id: string, +/** + * 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; + container: IOdspFluidContainer; services: OdspContainerServices; - }> { - const url = createOdspUrl({ - siteUrl: this.connectionConfig.siteUrl, - driveId: this.connectionConfig.driveId, - itemId: id, - dataStorePath: "", - }); - return super.getContainer(url, containerSchema); - } + }>; + /** + * Opens existing container. If container does not exist, the call will fail with an error with errorType = DriverErrorTypes.fileNotFoundOrAccessDeniedError. + * @param request - identification of the container + * @param containerSchema - schema of the container. + */ + getContainer( + request: OdspGetContainerArgType, + containerSchema: T, + ): Promise<{ + container: IOdspFluidContainer; + services: OdspContainerServices; + }>; } /** * 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 OdspClientCore { - private readonly connectionConfig: OdspConnectionConfig; +class OdspClient implements IOdspClient { + private readonly configProvider: IConfigProviderBase; public constructor( private readonly documentServiceFactory: IDocumentServiceFactory, private readonly urlResolver: OdspDriverUrlResolver, + protected readonly connectionConfig: OdspSiteIdentification, private readonly logger?: ITelemetryBaseLogger, - private readonly configProvider?: IConfigProviderBase, + configProvider?: IConfigProviderBase, ) { - this.connectionConfig = properties.connection; + this.configProvider = wrapConfigProvider(configProvider); } public async createContainer( containerSchema: T, ): Promise<{ - container: IFluidContainer; + container: IOdspFluidContainer; services: OdspContainerServices; }> { const loader = this.createLoader(containerSchema); @@ -191,31 +184,44 @@ export class OdspClientCore { }); const rootDataObject = await this.getContainerEntryPoint(container); - const fluidContainer = createFluidContainer({ container, rootDataObject }); + const fluidContainer = createFluidContainer({ + container, + rootDataObject, + }) as IOdspFluidContainer; - this.addAttachCallback(container, fluidContainer); + OdspClient.addAttachCallback(container, fluidContainer, this.connectionConfig); const services = await this.getContainerServices(container); - return { container: fluidContainer as IFluidContainer, services }; + return { container: fluidContainer, services }; } public async getContainer( - url: string, + request: OdspGetContainerArgType, containerSchema: T, ): Promise<{ - container: IFluidContainer; + container: IOdspFluidContainer; services: OdspContainerServices; }> { const loader = this.createLoader(containerSchema); - const container = await loader.resolve({ url }); + const container = await loader.resolve({ + url: createOdspUrl({ + siteUrl: this.connectionConfig.siteUrl, + driveId: this.connectionConfig.driveId, + itemId: request.itemId, + dataStorePath: "", + }), + headers: { + [SharingLinkHeader.isSharingLinkToRedeem]: request.sharingLinkToRedeem !== undefined, + }, + }); - const fluidContainer = createFluidContainer({ + 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 { @@ -251,23 +257,34 @@ export class OdspClientCore { }); } - private addAttachCallback( + private static addAttachCallback( container: IContainer, - fluidContainer: IFluidContainer, + fluidContainer: IOdspFluidContainer, + connectionConfig: OdspSiteIdentification, ): void { - /** * See {@link FluidContainer.attach} */ fluidContainer.attach = async ( - odspProps?: ContainerAttachProps, - ): Promise => { - const createNewRequest: IRequest = createOdspCreateContainerRequest( - this.connectionConfig.siteUrl, - this.connectionConfig.driveId, - odspProps?.filePath ?? "", - odspProps?.fileName ?? uuid(), - ); + odspProps?: OdspContainerAttachArgType, + ): Promise => { + const createNewRequest: IRequest = + odspProps !== undefined && "itemId" in odspProps + ? { + url: createOdspUrl({ + siteUrl: connectionConfig.siteUrl, + driveId: connectionConfig.driveId, + itemId: odspProps.itemId, + dataStorePath: "", + }), + } + : createOdspCreateContainerRequest( + connectionConfig.siteUrl, + connectionConfig.driveId, + odspProps?.filePath ?? "", + odspProps?.fileName ?? uuid(), + odspProps?.createShareLinkType, + ); if (container.attachState !== AttachState.Detached) { throw new Error("Cannot attach container. Container is not in detached state"); } @@ -284,7 +301,7 @@ export class OdspClientCore { * 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 }; }; } 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..38870f9cd503 100644 --- a/packages/service-clients/odsp-client/src/test/odspClient.spec.ts +++ b/packages/service-clients/odsp-client/src/test/odspClient.spec.ts @@ -11,7 +11,7 @@ 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 IOdspClient, 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/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts b/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts index a7de506f19d9..5305f2aaca3f 100644 --- a/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts +++ b/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts @@ -112,7 +112,7 @@ export class TinyliciousClient { return container.resolvedUrl.id; }; - const fluidContainer = createFluidContainer({ + const fluidContainer = createFluidContainer Promise>({ container, rootDataObject, }); @@ -140,7 +140,7 @@ export class TinyliciousClient { const loader = this.createLoader(containerSchema, compatibilityMode); const container = await loader.resolve({ url: id }); const rootDataObject = await this.getContainerEntryPoint(container); - const fluidContainer = createFluidContainer({ + const fluidContainer = createFluidContainer Promise>({ container, rootDataObject, }); From e7a260c171b88b79ed236440e6ef5e378d945a42 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Mon, 5 Aug 2024 14:21:36 -0700 Subject: [PATCH 03/10] unfinished --- .../odsp-client/src/odspClient.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index 2b098f7dec46..a04ec22f6b54 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -32,6 +32,8 @@ import { createOdspUrl, isOdspResolvedUrl, SharingLinkHeader, + type OdspFluidDataStoreLocator, + storeLocatorInOdspUrl, } from "@fluidframework/odsp-driver/internal"; import type { OdspResourceTokenFetchOptions } from "@fluidframework/odsp-driver-definitions/internal"; import { wrapConfigProviderWithDefaults } from "@fluidframework/telemetry-utils/internal"; @@ -204,13 +206,19 @@ class OdspClient implements IOdspClient { services: OdspContainerServices; }> { const loader = this.createLoader(containerSchema); + + const locator: OdspFluidDataStoreLocator = { + siteUrl: this.connectionConfig.siteUrl, + driveId: this.connectionConfig.driveId, + itemId: request.itemId, + dataStorePath: "", + } + const url = new URL(baseUrl); + storeLocatorInOdspUrl(url, locator); + // return url.href; + const container = await loader.resolve({ - url: createOdspUrl({ - siteUrl: this.connectionConfig.siteUrl, - driveId: this.connectionConfig.driveId, - itemId: request.itemId, - dataStorePath: "", - }), + url: createOdspUrl(locator), headers: { [SharingLinkHeader.isSharingLinkToRedeem]: request.sharingLinkToRedeem !== undefined, }, From fdb6e21bcd9b573485f48ae6ad07d762ea38db5a Mon Sep 17 00:00:00 2001 From: "REDMOND\\vladsud" Date: Mon, 5 Aug 2024 16:29:38 -0700 Subject: [PATCH 04/10] sharing links prototype --- .../api-report/odsp-client.alpha.api.md | 5 +- .../odsp-client/src/interfaces.ts | 53 ++++++++++++----- .../odsp-client/src/odspClient.ts | 57 +++++++++++++------ 3 files changed, 81 insertions(+), 34 deletions(-) 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 9d9c5eb64294..4fc2f2501949 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 @@ -58,6 +58,7 @@ export type OdspContainerAttachArgType = { export interface OdspContainerAttachReturnType { itemId: string; shareLinkInfo?: ShareLinkInfoType; + sharingLink?: string; } // @alpha @@ -71,7 +72,9 @@ export interface OdspContainerServices { // @alpha export type OdspGetContainerArgType = { itemId: string; -} | IRequest; +} | { + sharingLinkToRedeem: string; +}; // @alpha export interface OdspMember extends IMember { diff --git a/packages/service-clients/odsp-client/src/interfaces.ts b/packages/service-clients/odsp-client/src/interfaces.ts index f73277ac8476..3381a6341771 100644 --- a/packages/service-clients/odsp-client/src/interfaces.ts +++ b/packages/service-clients/odsp-client/src/interfaces.ts @@ -6,7 +6,6 @@ import type { IConfigProviderBase, ITelemetryBaseLogger, - IRequest, } from "@fluidframework/core-interfaces"; import type { IMember, IServiceAudience } from "@fluidframework/fluid-static"; import type { @@ -82,10 +81,7 @@ export interface OdspClientProps { /** * Argument type of IFluidContainer.attach() for containers created by IOdspClient * Specifies location / name of the file. - * 1. If undefined, file with random name (uuid) will be created. - * 2. Specifies file path / file name to be created. If 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 - * 3. (Microsoft internal only) files supporting FF format on alternate parition could point to existing file. + * If no argument is provided, file with random name (uuid) will be created. * @alpha */ export type OdspContainerAttachArgType = @@ -97,6 +93,8 @@ export type OdspContainerAttachArgType = /** * 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 | undefined; @@ -106,6 +104,9 @@ export type OdspContainerAttachArgType = createShareLinkType?: ISharingLinkKind; } | { + /** + * (Microsoft internal only) Files supporting FF format on alternate partition could point to existing file. + */ itemId: string; }; @@ -118,15 +119,26 @@ export interface OdspContainerAttachReturnType { * An ID of the document created. This ID could be passed to future IOdspClient.getContainer() call */ itemId: string; + /** * If OdspContainerAttachArgType.createShareLinkType was provided at the time of IOdspFluidContainer.attach() call, - * this value will contain sharing link to just created file. + * this value will contain sharing link information for created file. */ shareLinkInfo?: ShareLinkInfoType; + + /** + * If sharing link info was requested to be generated and successfully was obtained, this property will + * contain sharing link that could be used with IOdspClient.getContainer() to open such container by anyone + * who poses such link (and is within the sharing scope of a link) + * This link is sufficient to identify a file in Sharepoint. In other words, it encodes information like driveId, itemId, siteUrl. + */ + sharingLink?: string; } /** * IFluidContainer.attach() function signature for IOdspClient + * @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. * @alpha */ export type OdspContainerAttachType = ( @@ -137,15 +149,26 @@ export type OdspContainerAttachType = ( * Type of argument to IOdspClient.getContainer() * @alpha */ -export interface OdspGetContainerArgType { - itemId: string; - - /** - * 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; -} +export type OdspGetContainerArgType = + | { + /** + * If itemId is provided, then OdspSiteIdentification information (see OdspClientProps.connection) passed to createOdspClient() + * is used together with itemId to identify a file in Sharepoint. + */ + itemId: string; + } + | { + /** + * A sharing link could be provided to identify a file. This link has to be in very specific format - see + * OdspContainerAttachReturnType.sharingLink, result of calling IOdspFluidContainer. + * When sharing link is provided, it uniquely identifies a file in Sharepoint - OdspSiteIdentification 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; + }; /** * OdspContainerServices is returned by the OdspClient alongside a FluidContainer. It holds the diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index a04ec22f6b54..5bf1c1d2cc20 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -199,7 +199,7 @@ class OdspClient implements IOdspClient { } public async getContainer( - request: OdspGetContainerArgType, + containerIdentity: OdspGetContainerArgType, containerSchema: T, ): Promise<{ container: IOdspFluidContainer; @@ -207,22 +207,25 @@ class OdspClient implements IOdspClient { }> { const loader = this.createLoader(containerSchema); - const locator: OdspFluidDataStoreLocator = { - siteUrl: this.connectionConfig.siteUrl, - driveId: this.connectionConfig.driveId, - itemId: request.itemId, - dataStorePath: "", - } - const url = new URL(baseUrl); - storeLocatorInOdspUrl(url, locator); - // return url.href; - - const container = await loader.resolve({ - url: createOdspUrl(locator), - headers: { - [SharingLinkHeader.isSharingLinkToRedeem]: request.sharingLinkToRedeem !== undefined, - }, - }); + const request: IRequest = + "itemId" in containerIdentity + ? { + url: createOdspUrl({ + siteUrl: this.connectionConfig.siteUrl, + driveId: this.connectionConfig.driveId, + itemId: containerIdentity.itemId, + dataStorePath: "", + }), + } + : { + url: containerIdentity.sharingLinkToRedeem, + headers: { + [SharingLinkHeader.isSharingLinkToRedeem]: + containerIdentity.sharingLinkToRedeem !== undefined, + }, + }; + + const container = await loader.resolve(request); const fluidContainer = createFluidContainer({ container, @@ -309,7 +312,25 @@ class OdspClient implements IOdspClient { * 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 { itemId: resolvedUrl.itemId, shareLinkInfo: resolvedUrl.shareLinkInfo }; + + let sharingLink: string | undefined; + if (resolvedUrl.shareLinkInfo?.createLink?.link) { + const url = new URL(resolvedUrl.shareLinkInfo?.createLink?.link?.webUrl); + const locator: OdspFluidDataStoreLocator = { + siteUrl: connectionConfig.siteUrl, + driveId: connectionConfig.driveId, + itemId: resolvedUrl.itemId, + dataStorePath: "", + }; + storeLocatorInOdspUrl(url, locator); + sharingLink = url.href; + } + + return { + itemId: resolvedUrl.itemId, + sharingLink, + shareLinkInfo: resolvedUrl.shareLinkInfo, + }; }; } From b49ecac3bbe70362c9e1102cb5a00c148e841561 Mon Sep 17 00:00:00 2001 From: "REDMOND\\vladsud" Date: Mon, 5 Aug 2024 19:59:58 -0700 Subject: [PATCH 05/10] type renames, CLP --- .../api-report/odsp-client.alpha.api.md | 24 +++--- .../api-report/odsp-client.beta.api.md | 4 +- .../service-clients/odsp-client/src/index.ts | 8 +- .../odsp-client/src/interfaces.ts | 26 ++++--- .../odsp-client/src/odspClient.ts | 74 +++++++++++-------- 5 files changed, 77 insertions(+), 59 deletions(-) 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 4fc2f2501949..ed6db892fbee 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 @@ -16,7 +16,7 @@ export interface IOdspClient { container: IOdspFluidContainer; services: OdspContainerServices; }>; - getContainer(request: OdspGetContainerArgType, containerSchema: T): Promise<{ + getContainer(request: OdspContainerIdentifier, containerSchema: T, isClpCompliant?: boolean): Promise<{ container: IOdspFluidContainer; services: OdspContainerServices; }>; @@ -41,12 +41,12 @@ export interface OdspClientProps { } // @beta -export interface OdspConnectionConfig extends OdspSiteIdentification { +export interface OdspConnectionConfig extends OdspSiteLocation { tokenProvider: IOdspTokenProvider; } // @alpha -export type OdspContainerAttachArgType = { +export type OdspContainerAttachInfo = { filePath?: string | undefined; fileName?: string | undefined; createShareLinkType?: ISharingLinkKind; @@ -55,27 +55,27 @@ export type OdspContainerAttachArgType = { }; // @alpha -export interface OdspContainerAttachReturnType { +export interface OdspContainerAttachResult { itemId: string; shareLinkInfo?: ShareLinkInfoType; sharingLink?: string; } // @alpha -export type OdspContainerAttachType = (param?: OdspContainerAttachArgType) => Promise; +export type OdspContainerAttachType = (param?: OdspContainerAttachInfo, isClpCompliant?: boolean) => Promise; // @alpha -export interface OdspContainerServices { - audience: IOdspAudience; -} - -// @alpha -export type OdspGetContainerArgType = { +export type OdspContainerIdentifier = { itemId: string; } | { sharingLinkToRedeem: string; }; +// @alpha +export interface OdspContainerServices { + audience: IOdspAudience; +} + // @alpha export interface OdspMember extends IMember { email: string; @@ -84,7 +84,7 @@ export interface OdspMember extends IMember { } // @beta -export interface OdspSiteIdentification { +export interface OdspSiteLocation { driveId: string; siteUrl: string; } 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 7c0bdcb2cefe..58c6c5beedb0 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 @@ -11,12 +11,12 @@ export interface IOdspTokenProvider { } // @beta -export interface OdspConnectionConfig extends OdspSiteIdentification { +export interface OdspConnectionConfig extends OdspSiteLocation { tokenProvider: IOdspTokenProvider; } // @beta -export interface OdspSiteIdentification { +export interface OdspSiteLocation { driveId: string; siteUrl: string; } diff --git a/packages/service-clients/odsp-client/src/index.ts b/packages/service-clients/odsp-client/src/index.ts index f0fc4467de1e..aa2b0409534c 100644 --- a/packages/service-clients/odsp-client/src/index.ts +++ b/packages/service-clients/odsp-client/src/index.ts @@ -14,17 +14,17 @@ */ export type { - OdspSiteIdentification, + OdspSiteLocation, OdspConnectionConfig, OdspClientProps, OdspContainerServices, IOdspAudience, OdspMember, TokenResponse, - OdspContainerAttachArgType, + OdspContainerAttachInfo, OdspContainerAttachType, - OdspContainerAttachReturnType, - OdspGetContainerArgType, + OdspContainerAttachResult, + OdspContainerIdentifier, } from "./interfaces.js"; export { type IOdspClient, diff --git a/packages/service-clients/odsp-client/src/interfaces.ts b/packages/service-clients/odsp-client/src/interfaces.ts index 3381a6341771..1ccdfba779a9 100644 --- a/packages/service-clients/odsp-client/src/interfaces.ts +++ b/packages/service-clients/odsp-client/src/interfaces.ts @@ -23,7 +23,7 @@ import type { IOdspTokenProvider } from "./token.js"; * required for ODSP. * @beta */ -export interface OdspSiteIdentification { +export interface OdspSiteLocation { /** * Site url representing ODSP resource location. It points to the specific SharePoint site where you can store and access the containers you create. */ @@ -41,7 +41,7 @@ export interface OdspSiteIdentification { * required for ODSP. * @beta */ -export interface OdspConnectionConfig extends OdspSiteIdentification { +export interface OdspConnectionConfig extends OdspSiteLocation { /** * Instance that provides AAD endpoint tokens for Push and SharePoint */ @@ -84,7 +84,7 @@ export interface OdspClientProps { * If no argument is provided, file with random name (uuid) will be created. * @alpha */ -export type OdspContainerAttachArgType = +export type OdspContainerAttachInfo = | { /** * The file path where Fluid containers are created. If undefined, the file is created at the root. @@ -114,14 +114,14 @@ export type OdspContainerAttachArgType = * An object type returned by IOdspFluidContainer.attach() call. * * @alpha */ -export interface OdspContainerAttachReturnType { +export interface OdspContainerAttachResult { /** * An ID of the document created. This ID could be passed to future IOdspClient.getContainer() call */ itemId: string; /** - * If OdspContainerAttachArgType.createShareLinkType was provided at the time of IOdspFluidContainer.attach() call, + * If OdspContainerAttachInfo.createShareLinkType was provided at the time of IOdspFluidContainer.attach() call, * this value will contain sharing link information for created file. */ shareLinkInfo?: ShareLinkInfoType; @@ -139,20 +139,24 @@ export interface OdspContainerAttachReturnType { * IFluidContainer.attach() function signature for IOdspClient * @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 isClpCompliant - 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. * @alpha */ export type OdspContainerAttachType = ( - param?: OdspContainerAttachArgType, -) => Promise; + param?: OdspContainerAttachInfo, + isClpCompliant?: boolean, +) => Promise; /** * Type of argument to IOdspClient.getContainer() + * Identifies a container instance in storage. * @alpha */ -export type OdspGetContainerArgType = +export type OdspContainerIdentifier = | { /** - * If itemId is provided, then OdspSiteIdentification information (see OdspClientProps.connection) passed to createOdspClient() + * If itemId is provided, then OdspSiteLocation information (see OdspClientProps.connection) passed to createOdspClient() * is used together with itemId to identify a file in Sharepoint. */ itemId: string; @@ -160,8 +164,8 @@ export type OdspGetContainerArgType = | { /** * A sharing link could be provided to identify a file. This link has to be in very specific format - see - * OdspContainerAttachReturnType.sharingLink, result of calling IOdspFluidContainer. - * When sharing link is provided, it uniquely identifies a file in Sharepoint - OdspSiteIdentification information + * OdspContainerAttachResult.sharingLink, result of calling IOdspFluidContainer. + * When sharing link is provided, it uniquely identifies a file in Sharepoint - OdspSiteLocation 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 diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index 5bf1c1d2cc20..c553b50a5945 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -32,6 +32,7 @@ import { createOdspUrl, isOdspResolvedUrl, SharingLinkHeader, + ClpCompliantAppHeader, type OdspFluidDataStoreLocator, storeLocatorInOdspUrl, } from "@fluidframework/odsp-driver/internal"; @@ -42,12 +43,12 @@ import { v4 as uuid } from "uuid"; import type { TokenResponse, OdspClientProps, - OdspSiteIdentification, - OdspContainerAttachArgType, + OdspSiteLocation, + OdspContainerAttachInfo, OdspContainerAttachType, OdspContainerServices, - OdspContainerAttachReturnType, - OdspGetContainerArgType, + OdspContainerAttachResult, + OdspContainerIdentifier, } from "./interfaces.js"; import { createOdspAudienceMember } from "./odspAudience.js"; import { type IOdspTokenProvider } from "./token.js"; @@ -91,7 +92,7 @@ function wrapConfigProvider(baseConfigProvider?: IConfigProviderBase): IConfigPr function createOdspClientCore( driverFactory: IDocumentServiceFactory, urlResolver: OdspDriverUrlResolver, - connectionConfig: OdspSiteIdentification, + connectionConfig: OdspSiteLocation, logger?: ITelemetryBaseLogger, configProvider?: IConfigProviderBase, ): IOdspClient { @@ -145,11 +146,14 @@ export interface IOdspClient { /** * Opens existing container. If container does not exist, the call will fail with an error with errorType = DriverErrorTypes.fileNotFoundOrAccessDeniedError. * @param request - identification of the container + * @param isClpCompliant - 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. * @param containerSchema - schema of the container. */ getContainer( - request: OdspGetContainerArgType, + request: OdspContainerIdentifier, containerSchema: T, + isClpCompliant?: boolean, ): Promise<{ container: IOdspFluidContainer; services: OdspContainerServices; @@ -165,7 +169,7 @@ class OdspClient implements IOdspClient { public constructor( private readonly documentServiceFactory: IDocumentServiceFactory, private readonly urlResolver: OdspDriverUrlResolver, - protected readonly connectionConfig: OdspSiteIdentification, + protected readonly connectionConfig: OdspSiteLocation, private readonly logger?: ITelemetryBaseLogger, configProvider?: IConfigProviderBase, ) { @@ -199,33 +203,34 @@ class OdspClient implements IOdspClient { } public async getContainer( - containerIdentity: OdspGetContainerArgType, + containerIdentity: OdspContainerIdentifier, containerSchema: T, + isClpCompliant?: boolean, ): Promise<{ container: IOdspFluidContainer; services: OdspContainerServices; }> { const loader = this.createLoader(containerSchema); - const request: IRequest = - "itemId" in containerIdentity - ? { - url: createOdspUrl({ - siteUrl: this.connectionConfig.siteUrl, - driveId: this.connectionConfig.driveId, - itemId: containerIdentity.itemId, - dataStorePath: "", - }), - } - : { - url: containerIdentity.sharingLinkToRedeem, - headers: { - [SharingLinkHeader.isSharingLinkToRedeem]: - containerIdentity.sharingLinkToRedeem !== undefined, - }, - }; - - const container = await loader.resolve(request); + const headers = {}; + if (isClpCompliant === true) { + headers[ClpCompliantAppHeader.isClpCompliantApp] = true; + } + + let url: string; + if ("itemId" in containerIdentity) { + url = createOdspUrl({ + siteUrl: this.connectionConfig.siteUrl, + driveId: this.connectionConfig.driveId, + itemId: containerIdentity.itemId, + dataStorePath: "", + }); + } else { + headers[SharingLinkHeader.isSharingLinkToRedeem] = true; + url = containerIdentity.sharingLinkToRedeem; + } + + const container = await loader.resolve({ url, headers }); const fluidContainer = createFluidContainer({ container, @@ -271,14 +276,15 @@ class OdspClient implements IOdspClient { private static addAttachCallback( container: IContainer, fluidContainer: IOdspFluidContainer, - connectionConfig: OdspSiteIdentification, + connectionConfig: OdspSiteLocation, ): void { /** * See {@link FluidContainer.attach} */ fluidContainer.attach = async ( - odspProps?: OdspContainerAttachArgType, - ): Promise => { + odspProps?: OdspContainerAttachInfo, + isClpCompliant?: boolean, + ): Promise => { const createNewRequest: IRequest = odspProps !== undefined && "itemId" in odspProps ? { @@ -296,6 +302,14 @@ class OdspClient implements IOdspClient { odspProps?.fileName ?? uuid(), odspProps?.createShareLinkType, ); + + if (isClpCompliant === true) { + if (createNewRequest.headers === undefined) { + createNewRequest.headers = {}; + } + createNewRequest.headers[ClpCompliantAppHeader.isClpCompliantApp] = true; + } + if (container.attachState !== AttachState.Detached) { throw new Error("Cannot attach container. Container is not in detached state"); } From ee00e8e391489c68110e128ca6a48be0d70ac2e5 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Wed, 7 Aug 2024 17:42:21 -0700 Subject: [PATCH 06/10] Introduce IOdspCreateRequest & IOdspOpenRequest and changing OdspClient workflows to operate with these types instead of IRequest. --- ...dsp-driver-definitions.legacy.alpha.api.md | 47 +++- .../odsp-driver-definitions/package.json | 9 +- .../odsp-driver-definitions/src/index.ts | 2 + .../src/resolvedUrl.ts | 165 ++++++++++++- ...OdspDriverDefinitionsPrevious.generated.ts | 2 + .../drivers/odsp-driver/src/createFile.ts | 54 +++-- .../drivers/odsp-driver/src/createOdspUrl.ts | 2 +- packages/drivers/odsp-driver/src/index.ts | 6 +- .../src/odspDocumentServiceFactoryCore.ts | 48 +--- .../odsp-driver/src/odspDriverUrlResolver.ts | 222 +++++++++++++++--- .../src/odspDriverUrlResolverForShareLink.ts | 18 +- .../api-report/odsp-client.alpha.api.md | 26 +- .../service-clients/odsp-client/src/index.ts | 2 + .../odsp-client/src/interfaces.ts | 77 ++++-- .../odsp-client/src/odspClient.ts | 190 ++++++++------- 15 files changed, 614 insertions(+), 256 deletions(-) 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 917e154c7b3e..b346c78a970e 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 @@ -58,6 +58,23 @@ export interface IFileEntry { resolvedUrl: IResolvedUrl; } +// @alpha +export type IOdspCreateRequest = { + siteUrl: string; + driveId: string; + dataStorePath?: string; + codeHint?: { + containerPackageName?: string; + }; + isClpCompliantApp?: boolean; +} & ({ + itemId: string; +} | { + filePath?: string; + fileName: string; + createShareLinkType?: ISharingLinkKind; +}); + // @alpha export interface IOdspError extends Omit, IOdspErrorAugmentations { // (undocumented) @@ -71,15 +88,29 @@ export interface IOdspErrorAugmentations { serverEpoch?: string; } +// @alpha +export interface IOdspOpenRequest { + // (undocumented) + codeHint?: { + containerPackageName?: string; + }; + dataStorePath?: string; + 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 +119,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 +134,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 +257,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/index.ts b/packages/drivers/odsp-driver-definitions/src/index.ts index 847555848019..97c4798c674a 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, + IOdspOpenRequest, + IOdspCreateRequest, 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..5d79b4668bba 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,40 @@ 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; + /* + * 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. + */ 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 privided, 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 +185,119 @@ 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; } + +/** + * Input arguments required to create IOdspResolvedUrl that OdspDriver can work with. + * @legacy + * @alpha + */ +export interface IOdspOpenRequest { + /** + * {@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; + + /** + * {@inheritDoc (IOdspResolvedUrl:interface).dataStorePath} + */ + dataStorePath?: string; + + /** + * {@inheritDoc (IOdspResolvedUrl:interface).codeHint} + */ + codeHint?: { + containerPackageName?: string; + }; +} + +/** + * Input arguments required to create IOdspResolvedUrl that OdspDriver can work with. + * @legacy + * @alpha + */ +export type IOdspCreateRequest = { + /** + * {@inheritDoc (IOdspUrlParts:interface).siteUrl} + */ + siteUrl: string; + + /** + * {@inheritDoc (IOdspUrlParts:interface).driveId} + */ + driveId: string; + + /** + * {@inheritDoc (IOdspResolvedUrl:interface).dataStorePath} + */ + dataStorePath?: string; + + /** + * {@inheritDoc (IOdspResolvedUrl:interface).codeHint} + */ + codeHint?: { + containerPackageName?: string; + }; + + /** + * {@inheritDoc (IOdspResolvedUrl:interface).isClpCompliantApp} + */ + isClpCompliantApp?: boolean; +} & ( + | { + /** + * {@inheritDoc (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..666e7000696d 100644 --- a/packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts +++ b/packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts @@ -21,13 +21,11 @@ import { IOdspUrlParts, IPersistedCache, IRelaySessionAwareDriverFactory, - ISharingLinkKind, ISocketStorageDiscovery, OdspResourceTokenFetchOptions, - SharingLinkRole, - SharingLinkScope, TokenFetchOptions, TokenFetcher, + ISharingLinkKind, } from "@fluidframework/odsp-driver-definitions/internal"; import { PerformanceEvent, createChildLogger } from "@fluidframework/telemetry-utils/internal"; import { v4 as uuid } from "uuid"; @@ -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..57a4a209c1e2 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, + IOdspOpenRequest, + IOdspCreateRequest, 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,144 @@ 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; +} + +/** + * Creates IOdspResolvedUrl from IOdspOpenRequest + * @param input - IOdspOpenRequest + * @returns IOdspResolvedUrl + * @alpha + */ +export async function createOpenOdspResolvedUrl( + input: IOdspOpenRequest, +): Promise { + const { siteUrl, driveId, itemId, fileVersion, dataStorePath = "", codeHint } = 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), + }; + + return { + ...input, + + type: "fluid", + odspResolvedUrl: true, + + id: hashedDocumentId, + hashedDocumentId, + + tokens: {}, + + // only used in create-new flows + fileName: "", + + url: `https://placeholder/placeholder/${hashedDocumentId}/${removeBeginningSlash(dataStorePath)}`, + + endpoints, + + shareLinkInfo: { + sharingLinkToRedeem, + }, + + dataStorePath, + codeHint, + }; +} + +/** + * Creates IOdspResolvedUrl from IOdspOpenRequest + * @param input - IOdspOpenRequest + * @returns IOdspResolvedUrl + * @alpha + */ +export async function createCreateOdspResolvedUrl( + input: IOdspCreateRequest, +): Promise { + const { + siteUrl, + dataStorePath, + codeHint, + itemId = "", + fileName = "", + filePath = "", + createShareLinkType, + } = input as IOdspCreateRequest & { + 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, + + // Is url even used? + url: `https://${siteUrl}?&version=null`, + + endpoints: { + snapshotStorageUrl: "", + attachmentGETStorageUrl: "", + attachmentPOSTStorageUrl: "", + deltaStorageUrl: "", + }, + + shareLinkInfo: + createShareLinkType === undefined + ? undefined + : { + createLink: { + createKind: createShareLinkType, + }, + }, + + dataStorePath, + codeHint, + }; +} + /** * 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 +254,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,67 +265,37 @@ 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); + + return createCreateOdspResolvedUrl({ siteUrl: siteURL, - hashedDocumentId: "", driveId: driveID, - itemId: "", + filePath, fileName, - summarizer: false, codeHint: { containerPackageName: packageName ?? undefined, }, - fileVersion: undefined, - shareLinkInfo: undefined, + createShareLinkType: createKind, 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 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, + return createOpenOdspResolvedUrl({ siteUrl, driveId, itemId, dataStorePath: path, - fileName: "", summarizer, codeHint: { containerPackageName, }, fileVersion, isClpCompliantApp: request.headers?.[ClpCompliantAppHeader.isClpCompliantApp], - }; + }); } /** @@ -231,10 +346,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 +397,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/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 ed6db892fbee..99034ef40731 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 @@ -16,7 +16,7 @@ export interface IOdspClient { container: IOdspFluidContainer; services: OdspContainerServices; }>; - getContainer(request: OdspContainerIdentifier, containerSchema: T, isClpCompliant?: boolean): Promise<{ + getContainer(request: OdspContainerIdentifier, containerSchema: T, options?: OdspContainerOpenOptions): Promise<{ container: IOdspFluidContainer; services: OdspContainerServices; }>; @@ -47,8 +47,8 @@ export interface OdspConnectionConfig extends OdspSiteLocation { // @alpha export type OdspContainerAttachInfo = { - filePath?: string | undefined; - fileName?: string | undefined; + filePath?: string; + fileName?: string; createShareLinkType?: ISharingLinkKind; } | { itemId: string; @@ -62,14 +62,24 @@ export interface OdspContainerAttachResult { } // @alpha -export type OdspContainerAttachType = (param?: OdspContainerAttachInfo, isClpCompliant?: boolean) => Promise; +export type OdspContainerAttachType = (param?: OdspContainerAttachInfo, options?: OdspContainerCreateOptions) => Promise; // @alpha -export type OdspContainerIdentifier = { +export interface OdspContainerCreateOptions { + isClpCompliant?: boolean; +} + +// @alpha +export interface OdspContainerIdentifier { itemId: string; -} | { - sharingLinkToRedeem: string; -}; +} + +// @alpha +export interface OdspContainerOpenOptions { + fileVersion?: string; + isClpCompliant?: boolean; + sharingLinkToRedeem?: string; +} // @alpha export interface OdspContainerServices { diff --git a/packages/service-clients/odsp-client/src/index.ts b/packages/service-clients/odsp-client/src/index.ts index aa2b0409534c..356ec2e88355 100644 --- a/packages/service-clients/odsp-client/src/index.ts +++ b/packages/service-clients/odsp-client/src/index.ts @@ -25,6 +25,8 @@ export type { OdspContainerAttachType, OdspContainerAttachResult, OdspContainerIdentifier, + OdspContainerOpenOptions, + OdspContainerCreateOptions, } from "./interfaces.js"; export { type IOdspClient, diff --git a/packages/service-clients/odsp-client/src/interfaces.ts b/packages/service-clients/odsp-client/src/interfaces.ts index 1ccdfba779a9..74fa835440e4 100644 --- a/packages/service-clients/odsp-client/src/interfaces.ts +++ b/packages/service-clients/odsp-client/src/interfaces.ts @@ -89,14 +89,14 @@ export type OdspContainerAttachInfo = /** * The file path where Fluid containers are created. If undefined, the file is created at the root. */ - filePath?: string | undefined; + 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 | undefined; + fileName?: string; /** * If provided, will instrcuct Sharepoint to create a sharing link as part of file creation flow. @@ -139,13 +139,12 @@ export interface OdspContainerAttachResult { * IFluidContainer.attach() function signature for IOdspClient * @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 isClpCompliant - 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. + * @param options - options controlling creation. * @alpha */ export type OdspContainerAttachType = ( param?: OdspContainerAttachInfo, - isClpCompliant?: boolean, + options?: OdspContainerCreateOptions, ) => Promise; /** @@ -153,26 +152,54 @@ export type OdspContainerAttachType = ( * Identifies a container instance in storage. * @alpha */ -export type OdspContainerIdentifier = - | { - /** - * If itemId is provided, then OdspSiteLocation information (see OdspClientProps.connection) passed to createOdspClient() - * is used together with itemId to identify a file in Sharepoint. - */ - itemId: string; - } - | { - /** - * A sharing link could be provided to identify a file. This link has to be in very specific format - see - * OdspContainerAttachResult.sharingLink, result of calling IOdspFluidContainer. - * When sharing link is provided, it uniquely identifies a file in Sharepoint - OdspSiteLocation 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; - }; +export interface OdspContainerIdentifier { + /** + * If itemId is provided, then OdspSiteLocation information (see OdspClientProps.connection) passed to createOdspClient() + * is used together with itemId to identify a file in Sharepoint. + */ + itemId: string; +} + +/** + * 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, result of calling IOdspFluidContainer. + * When sharing link is provided, it uniquely identifies a file in Sharepoint - OdspSiteLocation 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; + + /** + * 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. + */ + isClpCompliant?: boolean; + + /** + * 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; +} + +/** + * Interface describing various options controling container open + * @alpha + */ +export interface OdspContainerCreateOptions { + /** + * 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. + */ + isClpCompliant?: boolean; +} /** * OdspContainerServices is returned by the OdspClient alongside a FluidContainer. It holds the diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index c553b50a5945..c2fc3ada1dfc 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -16,8 +16,12 @@ 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 { + 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 { @@ -27,16 +31,15 @@ import { } from "@fluidframework/fluid-static/internal"; import { OdspDocumentServiceFactory, - OdspDriverUrlResolver, - createOdspCreateContainerRequest, - createOdspUrl, isOdspResolvedUrl, - SharingLinkHeader, - ClpCompliantAppHeader, - type OdspFluidDataStoreLocator, - storeLocatorInOdspUrl, + createOpenOdspResolvedUrl, + createCreateOdspResolvedUrl, } from "@fluidframework/odsp-driver/internal"; -import type { OdspResourceTokenFetchOptions } from "@fluidframework/odsp-driver-definitions/internal"; +import type { + OdspResourceTokenFetchOptions, + IOdspOpenRequest, + IOdspCreateRequest, +} from "@fluidframework/odsp-driver-definitions/internal"; import { wrapConfigProviderWithDefaults } from "@fluidframework/telemetry-utils/internal"; import { v4 as uuid } from "uuid"; @@ -49,6 +52,8 @@ import type { OdspContainerServices, OdspContainerAttachResult, OdspContainerIdentifier, + OdspContainerOpenOptions, + OdspContainerCreateOptions, } from "./interfaces.js"; import { createOdspAudienceMember } from "./odspAudience.js"; import { type IOdspTokenProvider } from "./token.js"; @@ -84,6 +89,38 @@ function wrapConfigProvider(baseConfigProvider?: IConfigProviderBase): IConfigPr return wrapConfigProviderWithDefaults(baseConfigProvider, odspClientFeatureGates); } +class OdspFileOpenUrlResolver implements IUrlResolver { + public constructor(private readonly input: IOdspOpenRequest) {} + + 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?: IOdspCreateRequest; + + public constructor() {} + + public update(input: IOdspCreateRequest): 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 properties - properties @@ -91,12 +128,11 @@ function wrapConfigProvider(baseConfigProvider?: IConfigProviderBase): IConfigPr */ function createOdspClientCore( driverFactory: IDocumentServiceFactory, - urlResolver: OdspDriverUrlResolver, connectionConfig: OdspSiteLocation, logger?: ITelemetryBaseLogger, configProvider?: IConfigProviderBase, ): IOdspClient { - return new OdspClient(driverFactory, urlResolver, connectionConfig, logger, configProvider); + return new OdspClient(driverFactory, connectionConfig, logger, configProvider); } /** @@ -113,7 +149,6 @@ export function createOdspClient(properties: OdspClientProps): IOdspClient { properties.persistedCache, properties.hostPolicy, ), - new OdspDriverUrlResolver(), properties.connection, properties.logger, properties.configProvider, @@ -153,7 +188,7 @@ export interface IOdspClient { getContainer( request: OdspContainerIdentifier, containerSchema: T, - isClpCompliant?: boolean, + options?: OdspContainerOpenOptions, ): Promise<{ container: IOdspFluidContainer; services: OdspContainerServices; @@ -168,7 +203,6 @@ class OdspClient implements IOdspClient { public constructor( private readonly documentServiceFactory: IDocumentServiceFactory, - private readonly urlResolver: OdspDriverUrlResolver, protected readonly connectionConfig: OdspSiteLocation, private readonly logger?: ITelemetryBaseLogger, configProvider?: IConfigProviderBase, @@ -182,7 +216,8 @@ class OdspClient implements IOdspClient { container: IOdspFluidContainer; services: OdspContainerServices; }> { - const loader = this.createLoader(containerSchema); + const resolver = new OdspFileCreateUrlResolver(); + const loader = this.createLoader(containerSchema, resolver); const container = await loader.createDetachedContainer({ package: "no-dynamic-package", @@ -195,7 +230,7 @@ class OdspClient implements IOdspClient { rootDataObject, }) as IOdspFluidContainer; - OdspClient.addAttachCallback(container, fluidContainer, this.connectionConfig); + OdspClient.addAttachCallback(container, fluidContainer, this.connectionConfig, resolver); const services = await this.getContainerServices(container); @@ -205,32 +240,33 @@ class OdspClient implements IOdspClient { public async getContainer( containerIdentity: OdspContainerIdentifier, containerSchema: T, - isClpCompliant?: boolean, + options?: OdspContainerOpenOptions, ): Promise<{ container: IOdspFluidContainer; services: OdspContainerServices; }> { - const loader = this.createLoader(containerSchema); - - const headers = {}; - if (isClpCompliant === true) { - headers[ClpCompliantAppHeader.isClpCompliantApp] = true; - } - - let url: string; - if ("itemId" in containerIdentity) { - url = createOdspUrl({ - siteUrl: this.connectionConfig.siteUrl, - driveId: this.connectionConfig.driveId, - itemId: containerIdentity.itemId, - dataStorePath: "", - }); - } else { - headers[SharingLinkHeader.isSharingLinkToRedeem] = true; - url = containerIdentity.sharingLinkToRedeem; - } - - const container = await loader.resolve({ url, headers }); + const resolvedUrl: IOdspOpenRequest = { + summarizer: false, + + // Identity of a file + siteUrl: this.connectionConfig.siteUrl, + driveId: this.connectionConfig.driveId, + itemId: containerIdentity.itemId, + + fileVersion: options?.fileVersion, + + sharingLinkToRedeem: options?.sharingLinkToRedeem, + + isClpCompliantApp: options?.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, @@ -240,7 +276,7 @@ class OdspClient implements IOdspClient { return { container: fluidContainer, services }; } - private createLoader(schema: ContainerSchema): Loader { + private createLoader(schema: ContainerSchema, urlResolver: IUrlResolver): Loader { const runtimeFactory = createDOProviderContainerRuntimeFactory({ schema, compatibilityMode: "2", @@ -264,7 +300,7 @@ class OdspClient implements IOdspClient { }; return new Loader({ - urlResolver: this.urlResolver, + urlResolver, documentServiceFactory: this.documentServiceFactory, codeLoader, logger: this.logger, @@ -277,43 +313,43 @@ class OdspClient implements IOdspClient { container: IContainer, fluidContainer: IOdspFluidContainer, connectionConfig: OdspSiteLocation, + resolver: OdspFileCreateUrlResolver, ): void { /** * See {@link FluidContainer.attach} */ fluidContainer.attach = async ( odspProps?: OdspContainerAttachInfo, - isClpCompliant?: boolean, + options?: OdspContainerCreateOptions, ): Promise => { - const createNewRequest: IRequest = + if (container.attachState !== AttachState.Detached) { + throw new Error("Cannot attach container. Container is not in detached state"); + } + + const base = { + siteUrl: connectionConfig.siteUrl, + driveId: connectionConfig.driveId, + isClpCompliantApp: options?.isClpCompliant === true, + }; + + const resolved: IOdspCreateRequest = odspProps !== undefined && "itemId" in odspProps ? { - url: createOdspUrl({ - siteUrl: connectionConfig.siteUrl, - driveId: connectionConfig.driveId, - itemId: odspProps.itemId, - dataStorePath: "", - }), + ...base, + itemId: odspProps.itemId, } - : createOdspCreateContainerRequest( - connectionConfig.siteUrl, - connectionConfig.driveId, - odspProps?.filePath ?? "", - odspProps?.fileName ?? uuid(), - odspProps?.createShareLinkType, - ); - - if (isClpCompliant === true) { - if (createNewRequest.headers === undefined) { - createNewRequest.headers = {}; - } - createNewRequest.headers[ClpCompliantAppHeader.isClpCompliantApp] = true; - } + : { + ...base, + filePath: odspProps?.filePath ?? "", + fileName: odspProps?.fileName ?? uuid(), + createShareLinkType: odspProps?.createShareLinkType, + }; - if (container.attachState !== AttachState.Detached) { - throw new Error("Cannot attach container. Container is not in detached state"); - } - await container.attach(createNewRequest); + 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; @@ -321,28 +357,8 @@ class OdspClient implements IOdspClient { 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. - */ - - let sharingLink: string | undefined; - if (resolvedUrl.shareLinkInfo?.createLink?.link) { - const url = new URL(resolvedUrl.shareLinkInfo?.createLink?.link?.webUrl); - const locator: OdspFluidDataStoreLocator = { - siteUrl: connectionConfig.siteUrl, - driveId: connectionConfig.driveId, - itemId: resolvedUrl.itemId, - dataStorePath: "", - }; - storeLocatorInOdspUrl(url, locator); - sharingLink = url.href; - } - return { itemId: resolvedUrl.itemId, - sharingLink, shareLinkInfo: resolvedUrl.shareLinkInfo, }; }; From e270b9b2cb2b789b1e848485efd170ef43cd7241 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Wed, 7 Aug 2024 18:06:31 -0700 Subject: [PATCH 07/10] Interface cleanup --- .../odsp-client/shared-tree-demo/src/fluid.ts | 2 +- .../odsp-client/src/test/audience.spec.ts | 4 +- .../src/test/containerCreate.spec.ts | 4 +- .../odsp-client/src/test/ddsTests.spec.ts | 10 +- .../api-report/odsp-client.alpha.api.md | 29 ++--- .../api-report/odsp-client.beta.api.md | 9 +- .../service-clients/odsp-client/src/index.ts | 13 +- .../odsp-client/src/interfaces.ts | 115 +++++++++--------- .../odsp-client/src/odspClient.ts | 72 +++-------- .../odsp-client/src/test/odspClient.spec.ts | 4 +- 10 files changed, 103 insertions(+), 159 deletions(-) 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 b722da338195..8494ae22ba87 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 @@ -32,7 +32,7 @@ export const loadFluidData = async ( container, services, }: { container: IOdspFluidContainer; services: OdspContainerServices } = - await client.getContainer({ itemId }, schema); + await client.getContainer(itemId, schema); return { services, container }; }; 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 cccc7f7e9bca..dc7e5c62d69f 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 @@ -110,7 +110,7 @@ describe("Fluid audience", () => { "Fluid.Container.ForceWriteConnection": true, }), ); - const { services: servicesGet } = await client2.getContainer({ itemId }, schema); + const { services: servicesGet } = await client2.getContainer(itemId, schema); /* This is a workaround for a known bug, we should have one member (self) upon container connection */ const partner = await waitForMember(servicesGet.audience, client2Creds.email); @@ -154,7 +154,7 @@ describe("Fluid audience", () => { "Fluid.Container.ForceWriteConnection": true, }), ); - const { services: servicesGet } = await client2.getContainer({ itemId }, schema); + const { services: servicesGet } = await client2.getContainer(itemId, schema); /* This is a workaround for a known bug, we should have one member (self) upon container connection */ const partner = await waitForMember(servicesGet.audience, client2Creds.email); 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 516fd1c18768..1f74f149a232 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 @@ -126,7 +126,7 @@ describe("Container create scenarios", () => { }); } - const resources = client.getContainer({ itemId }, schema); + const resources = client.getContainer(itemId, schema); await assert.doesNotReject( resources, () => true, @@ -140,7 +140,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({ itemId: "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 c802a07ebf38..40383b0677b5 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 @@ -54,7 +54,7 @@ describe("Fluid data updates", () => { }); } - const resources = client.getContainer({ itemId }, schema); + const resources = client.getContainer(itemId, schema); await assert.doesNotReject( resources, () => true, @@ -91,7 +91,7 @@ describe("Fluid data updates", () => { map1Create.set("new-key", "new-value"); const valueCreate: string | undefined = map1Create.get("new-key"); - const { container: containerGet } = await client.getContainer({ itemId }, schema); + const { container: containerGet } = await client.getContainer(itemId, schema); const map1Get = containerGet.initialObjects.map1; const valueGet: string | undefined = await mapWait(map1Get, "new-key"); assert.strictEqual(valueGet, valueCreate, "container can't change initial objects"); @@ -130,7 +130,7 @@ describe("Fluid data updates", () => { "container returns the wrong type for mdo2", ); - const { container: containerGet } = await client.getContainer({ itemId }, doSchema); + const { container: containerGet } = await client.getContainer(itemId, doSchema); const initialObjectsGet = containerGet.initialObjects; assert( initialObjectsGet.mdo1 instanceof TestDataObject, @@ -182,7 +182,7 @@ describe("Fluid data updates", () => { "container returns the wrong type for mdo3", ); - const { container: containerGet } = await client.getContainer({ itemId }, doSchema); + const { container: containerGet } = await client.getContainer(itemId, doSchema); const initialObjectsGet = containerGet.initialObjects; assert( initialObjectsGet.mdo1 instanceof TestDataObject, @@ -229,7 +229,7 @@ describe("Fluid data updates", () => { }); } - const { container: containerGet } = await client.getContainer({ itemId }, doSchema); + const { container: containerGet } = await client.getContainer(itemId, doSchema); const initialObjectsGet = containerGet.initialObjects; const mdo2get: CounterTestDataObject = initialObjectsGet.mdo2; 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 99034ef40731..4cb46d282815 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 @@ -16,7 +16,7 @@ export interface IOdspClient { container: IOdspFluidContainer; services: OdspContainerServices; }>; - getContainer(request: OdspContainerIdentifier, containerSchema: T, options?: OdspContainerOpenOptions): Promise<{ + getContainer(itemId: string, containerSchema: T, options?: OdspContainerOpenOptions): Promise<{ container: IOdspFluidContainer; services: OdspContainerServices; }>; @@ -41,12 +41,15 @@ export interface OdspClientProps { } // @beta -export interface OdspConnectionConfig extends OdspSiteLocation { +export interface OdspConnectionConfig { + driveId: string; + isClpCompliant?: boolean; + siteUrl: string; tokenProvider: IOdspTokenProvider; } // @alpha -export type OdspContainerAttachInfo = { +export type OdspContainerAttachRequest = { filePath?: string; fileName?: string; createShareLinkType?: ISharingLinkKind; @@ -58,26 +61,14 @@ export type OdspContainerAttachInfo = { export interface OdspContainerAttachResult { itemId: string; shareLinkInfo?: ShareLinkInfoType; - sharingLink?: string; } // @alpha -export type OdspContainerAttachType = (param?: OdspContainerAttachInfo, options?: OdspContainerCreateOptions) => Promise; - -// @alpha -export interface OdspContainerCreateOptions { - isClpCompliant?: boolean; -} - -// @alpha -export interface OdspContainerIdentifier { - itemId: string; -} +export type OdspContainerAttachType = (param?: OdspContainerAttachRequest) => Promise; // @alpha export interface OdspContainerOpenOptions { fileVersion?: string; - isClpCompliant?: boolean; sharingLinkToRedeem?: string; } @@ -93,12 +84,6 @@ export interface OdspMember extends IMember { name: string; } -// @beta -export interface OdspSiteLocation { - driveId: string; - siteUrl: string; -} - // @beta export interface TokenResponse { fromCache?: boolean; 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 58c6c5beedb0..e7e1aeac7d85 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 @@ -11,14 +11,11 @@ export interface IOdspTokenProvider { } // @beta -export interface OdspConnectionConfig extends OdspSiteLocation { - tokenProvider: IOdspTokenProvider; -} - -// @beta -export interface OdspSiteLocation { +export interface OdspConnectionConfig { driveId: string; + isClpCompliant?: boolean; siteUrl: string; + tokenProvider: IOdspTokenProvider; } // @beta diff --git a/packages/service-clients/odsp-client/src/index.ts b/packages/service-clients/odsp-client/src/index.ts index 356ec2e88355..e2acdec9cc2e 100644 --- a/packages/service-clients/odsp-client/src/index.ts +++ b/packages/service-clients/odsp-client/src/index.ts @@ -14,23 +14,18 @@ */ export type { - OdspSiteLocation, OdspConnectionConfig, OdspClientProps, OdspContainerServices, IOdspAudience, OdspMember, TokenResponse, - OdspContainerAttachInfo, + OdspContainerAttachRequest, OdspContainerAttachType, OdspContainerAttachResult, - OdspContainerIdentifier, OdspContainerOpenOptions, - OdspContainerCreateOptions, + IOdspClient, + IOdspFluidContainer, } from "./interfaces.js"; -export { - type IOdspClient, - type IOdspFluidContainer, - createOdspClient, -} 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 74fa835440e4..145369e6f2bc 100644 --- a/packages/service-clients/odsp-client/src/interfaces.ts +++ b/packages/service-clients/odsp-client/src/interfaces.ts @@ -7,7 +7,12 @@ 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, @@ -23,7 +28,12 @@ import type { IOdspTokenProvider } from "./token.js"; * required for ODSP. * @beta */ -export interface OdspSiteLocation { +export interface OdspConnectionConfig { + /** + * Instance that provides AAD endpoint tokens for Push and SharePoint + */ + tokenProvider: IOdspTokenProvider; + /** * Site url representing ODSP resource location. It points to the specific SharePoint site where you can store and access the containers you create. */ @@ -33,19 +43,12 @@ export interface OdspSiteLocation { * SharePoint Embedded Container Id of the tenant where Fluid containers are created */ driveId: string; -} -/** - * Defines the necessary properties that will be applied to all containers - * created by an OdspClient instance. This includes callbacks for the authentication tokens - * required for ODSP. - * @beta - */ -export interface OdspConnectionConfig extends OdspSiteLocation { /** - * Instance that provides AAD endpoint tokens for Push and SharePoint + * 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. */ - tokenProvider: IOdspTokenProvider; + isClpCompliant?: boolean; } /** @@ -84,7 +87,7 @@ export interface OdspClientProps { * If no argument is provided, file with random name (uuid) will be created. * @alpha */ -export type OdspContainerAttachInfo = +export type OdspContainerAttachRequest = | { /** * The file path where Fluid containers are created. If undefined, the file is created at the root. @@ -121,18 +124,10 @@ export interface OdspContainerAttachResult { itemId: string; /** - * If OdspContainerAttachInfo.createShareLinkType was provided at the time of IOdspFluidContainer.attach() call, + * If OdspContainerAttachRequest.createShareLinkType was provided at the time of IOdspFluidContainer.attach() call, * this value will contain sharing link information for created file. */ shareLinkInfo?: ShareLinkInfoType; - - /** - * If sharing link info was requested to be generated and successfully was obtained, this property will - * contain sharing link that could be used with IOdspClient.getContainer() to open such container by anyone - * who poses such link (and is within the sharing scope of a link) - * This link is sufficient to identify a file in Sharepoint. In other words, it encodes information like driveId, itemId, siteUrl. - */ - sharingLink?: string; } /** @@ -143,23 +138,9 @@ export interface OdspContainerAttachResult { * @alpha */ export type OdspContainerAttachType = ( - param?: OdspContainerAttachInfo, - options?: OdspContainerCreateOptions, + param?: OdspContainerAttachRequest, ) => Promise; -/** - * Type of argument to IOdspClient.getContainer() - * Identifies a container instance in storage. - * @alpha - */ -export interface OdspContainerIdentifier { - /** - * If itemId is provided, then OdspSiteLocation information (see OdspClientProps.connection) passed to createOdspClient() - * is used together with itemId to identify a file in Sharepoint. - */ - itemId: string; -} - /** * Interface describing various options controling container open * @alpha @@ -168,7 +149,7 @@ 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, result of calling IOdspFluidContainer. - * When sharing link is provided, it uniquely identifies a file in Sharepoint - OdspSiteLocation information + * 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 @@ -176,12 +157,6 @@ export interface OdspContainerOpenOptions { */ sharingLinkToRedeem?: string; - /** - * 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. - */ - isClpCompliant?: boolean; - /** * 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. @@ -189,18 +164,6 @@ export interface OdspContainerOpenOptions { fileVersion?: string; } -/** - * Interface describing various options controling container open - * @alpha - */ -export interface OdspContainerCreateOptions { - /** - * 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. - */ - isClpCompliant?: boolean; -} - /** * OdspContainerServices is returned by the OdspClient alongside a FluidContainer. It holds the * functionality specifically tied to the ODSP service, and how the data stored in the @@ -259,3 +222,43 @@ export interface TokenResponse { */ fromCache?: boolean; } + +/** + * Fluid Container type + * @alpha + */ +export type IOdspFluidContainer = + IFluidContainer; + +/** + * 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: IOdspFluidContainer; + services: OdspContainerServices; + }>; + + /** + * 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: IOdspFluidContainer; + services: OdspContainerServices; + }>; +} diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index c2fc3ada1dfc..686e5b822616 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -22,7 +22,7 @@ import type { IUrlResolver, IResolvedUrl, } from "@fluidframework/driver-definitions/internal"; -import type { ContainerSchema, IFluidContainer } from "@fluidframework/fluid-static"; +import type { ContainerSchema } from "@fluidframework/fluid-static"; import type { IRootDataObject } from "@fluidframework/fluid-static/internal"; import { createDOProviderContainerRuntimeFactory, @@ -46,18 +46,20 @@ import { v4 as uuid } from "uuid"; import type { TokenResponse, OdspClientProps, - OdspSiteLocation, - OdspContainerAttachInfo, + OdspContainerAttachRequest, OdspContainerAttachType, OdspContainerServices, OdspContainerAttachResult, - OdspContainerIdentifier, OdspContainerOpenOptions, - OdspContainerCreateOptions, + OdspConnectionConfig, + IOdspClient, + IOdspFluidContainer, } from "./interfaces.js"; import { createOdspAudienceMember } from "./odspAudience.js"; import { type IOdspTokenProvider } from "./token.js"; +type OdspSiteLocation = Omit; + async function getStorageToken( options: OdspResourceTokenFetchOptions, tokenProvider: IOdspTokenProvider, @@ -123,8 +125,11 @@ class OdspFileCreateUrlResolver implements IUrlResolver { /** * Creates OdspClient - * @param properties - properties - * @returns 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, @@ -138,7 +143,7 @@ function createOdspClientCore( /** * Creates OdspClient * @param properties - properties - * @returns OdspClient + * @returns IOdspClient * @alpha */ export function createOdspClient(properties: OdspClientProps): IOdspClient { @@ -155,46 +160,6 @@ export function createOdspClient(properties: OdspClientProps): IOdspClient { ); } -/** - * Fluid Container type - * @alpha - */ -export type IOdspFluidContainer = - IFluidContainer; - -/** - * 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: IOdspFluidContainer; - services: OdspContainerServices; - }>; - - /** - * Opens existing container. If container does not exist, the call will fail with an error with errorType = DriverErrorTypes.fileNotFoundOrAccessDeniedError. - * @param request - identification of the container - * @param isClpCompliant - 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. - * @param containerSchema - schema of the container. - */ - getContainer( - request: OdspContainerIdentifier, - containerSchema: T, - options?: OdspContainerOpenOptions, - ): Promise<{ - container: IOdspFluidContainer; - services: OdspContainerServices; - }>; -} - /** * OdspClient provides the ability to have a Fluid object backed by the ODSP service within the context of Microsoft 365 (M365) tenants. */ @@ -238,7 +203,7 @@ class OdspClient implements IOdspClient { } public async getContainer( - containerIdentity: OdspContainerIdentifier, + itemId: string, containerSchema: T, options?: OdspContainerOpenOptions, ): Promise<{ @@ -251,13 +216,13 @@ class OdspClient implements IOdspClient { // Identity of a file siteUrl: this.connectionConfig.siteUrl, driveId: this.connectionConfig.driveId, - itemId: containerIdentity.itemId, + itemId, fileVersion: options?.fileVersion, sharingLinkToRedeem: options?.sharingLinkToRedeem, - isClpCompliantApp: options?.isClpCompliant === true, + isClpCompliantApp: this.connectionConfig.isClpCompliant === true, }; const loader = this.createLoader( @@ -319,8 +284,7 @@ class OdspClient implements IOdspClient { * See {@link FluidContainer.attach} */ fluidContainer.attach = async ( - odspProps?: OdspContainerAttachInfo, - options?: OdspContainerCreateOptions, + odspProps?: OdspContainerAttachRequest, ): Promise => { if (container.attachState !== AttachState.Detached) { throw new Error("Cannot attach container. Container is not in detached state"); @@ -329,7 +293,7 @@ class OdspClient implements IOdspClient { const base = { siteUrl: connectionConfig.siteUrl, driveId: connectionConfig.driveId, - isClpCompliantApp: options?.isClpCompliant === true, + isClpCompliantApp: connectionConfig.isClpCompliant === true, }; const resolved: IOdspCreateRequest = 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 38870f9cd503..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 { type IOdspClient, createOdspClient as createOdspClientCore } from "../odspClient.js"; +import type { OdspConnectionConfig, IOdspClient } from "../interfaces.js"; +import { createOdspClient as createOdspClientCore } from "../odspClient.js"; import { OdspTestTokenProvider } from "./odspTestTokenProvider.js"; From a53af7940cf3342cfe3a6f9465ea75c6746d5133 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Wed, 7 Aug 2024 18:17:25 -0700 Subject: [PATCH 08/10] Comments & typos --- .../odsp-driver-definitions/src/resolvedUrl.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts b/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts index 5d79b4668bba..df7233f192cc 100644 --- a/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts +++ b/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts @@ -161,7 +161,8 @@ export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { summarizer: boolean; /* - * containerPackageName is used for adding the package name to the request headers. + * 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?: { @@ -169,7 +170,7 @@ export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { }; /** - * If privided, tells version of a file to open + * If provided, tells version of a file to open */ fileVersion: string | undefined; @@ -193,7 +194,7 @@ export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { } /** - * Input arguments required to create IOdspResolvedUrl that OdspDriver can work with. + * A set of inputs to the driver for file open scenarios. * @legacy * @alpha */ @@ -247,7 +248,7 @@ export interface IOdspOpenRequest { } /** - * Input arguments required to create IOdspResolvedUrl that OdspDriver can work with. + * A set of inputs for the driver to create a new file or a FF partition on existing file. * @legacy * @alpha */ @@ -281,7 +282,9 @@ export type IOdspCreateRequest = { } & ( | { /** - * {@inheritDoc (IOdspUrlParts:interface).itemId} + * 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; } From 54e8fdf176c4c06d96b66b6d2ca527d05b141b8f Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Thu, 8 Aug 2024 16:26:52 -0700 Subject: [PATCH 09/10] Move to an attach delegate for Odsp client. --- .../odsp-client/shared-tree-demo/package.json | 1 + .../odsp-client/shared-tree-demo/src/fluid.ts | 19 ++--- .../shared-tree-demo/src/index.tsx | 14 ++-- .../shared-tree-demo/src/reactApp.tsx | 6 +- ...dsp-driver-definitions.legacy.alpha.api.md | 13 +--- .../odsp-driver-definitions/src/index.ts | 4 +- .../src/resolvedUrl.ts | 28 +------ .../odsp-driver/src/odspDriverUrlResolver.ts | 78 ++++++++++++------- .../api-report/fluid-framework.beta.api.md | 4 +- .../fluid-framework.legacy.alpha.api.md | 4 +- .../fluid-framework.legacy.public.api.md | 4 +- .../api-report/fluid-framework.public.api.md | 4 +- .../api-report/fluid-static.alpha.api.md | 4 +- .../api-report/fluid-static.beta.api.md | 4 +- .../api-report/fluid-static.public.api.md | 4 +- .../fluid-static/src/fluidContainer.ts | 28 +++---- .../azure-client/src/AzureClient.ts | 6 +- .../odsp-client/src/test/audience.spec.ts | 9 +-- .../src/test/containerCreate.spec.ts | 12 +-- .../odsp-client/src/test/ddsTests.spec.ts | 15 ++-- .../api-report/odsp-client.alpha.api.md | 16 ++-- .../service-clients/odsp-client/src/index.ts | 5 +- .../odsp-client/src/interfaces.ts | 32 ++++---- .../odsp-client/src/odspClient.ts | 54 +++++++------ .../src/TinyliciousClient.ts | 4 +- pnpm-lock.yaml | 3 + 26 files changed, 170 insertions(+), 205 deletions(-) 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/fluid.ts b/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts index 8494ae22ba87..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 @@ -6,10 +6,10 @@ import { createOdspClient, OdspContainerServices, - IOdspFluidContainer, + OdspContainerAttachFunctor, // eslint-disable-next-line import/no-internal-modules } from "@fluidframework/odsp-client/internal"; -import { ContainerSchema, SharedTree } from "fluid-framework"; +import { ContainerSchema, SharedTree, IFluidContainer } from "fluid-framework"; import { clientProps } from "./clientProps.js"; @@ -26,12 +26,12 @@ export const loadFluidData = async ( schema: ContainerSchema, ): Promise<{ services: OdspContainerServices; - container: IOdspFluidContainer; + container: IFluidContainer; }> => { const { container, services, - }: { container: IOdspFluidContainer; services: OdspContainerServices } = + }: { container: IFluidContainer; services: OdspContainerServices } = await client.getContainer(itemId, schema); return { services, container }; @@ -41,17 +41,14 @@ export const createFluidData = async ( schema: ContainerSchema, ): Promise<{ services: OdspContainerServices; - container: IOdspFluidContainer; + 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: IOdspFluidContainer; 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 ca0f47507aca..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 @@ -4,8 +4,10 @@ */ // eslint-disable-next-line import/no-internal-modules -import { IOdspFluidContainer } from "@fluidframework/odsp-client/internal"; -import { ITree } from "fluid-framework"; +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"; @@ -25,10 +27,11 @@ async function start(): Promise { // a new container. let itemId: string = location.hash.slice(1); const createNew = itemId.length === 0; - let container: IOdspFluidContainer; + let container: IFluidContainer; + let createFn: OdspContainerAttachFunctor | undefined; if (createNew) { - ({ container } = await createFluidData(containerSchema)); + ({ container, createFn } = await createFluidData(containerSchema)); } else { ({ container } = await loadFluidData(itemId, containerSchema)); } @@ -97,7 +100,8 @@ 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. - const res = 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 diff --git a/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx b/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx index a064d8728771..ec343f9381f3 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx +++ b/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx @@ -5,9 +5,7 @@ /* eslint-disable prefer-template */ -// eslint-disable-next-line import/no-internal-modules -import { IOdspFluidContainer } from "@fluidframework/odsp-client/internal"; -import { Tree, TreeView } from "fluid-framework"; +import { Tree, TreeView, IFluidContainer } from "fluid-framework"; import React, { ReactNode, useEffect, useState } from "react"; import { App, Letter } from "./schema.js"; @@ -130,7 +128,7 @@ function TopRow(props: { app: App }): JSX.Element { export function ReactApp(props: { data: TreeView; - container: IOdspFluidContainer; + container: IFluidContainer; cellSize: { x: number; y: number }; canvasSize: { x: number; y: number }; }): JSX.Element { 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 b346c78a970e..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 @@ -59,13 +59,9 @@ export interface IFileEntry { } // @alpha -export type IOdspCreateRequest = { +export type IOdspCreateArgs = { siteUrl: string; driveId: string; - dataStorePath?: string; - codeHint?: { - containerPackageName?: string; - }; isClpCompliantApp?: boolean; } & ({ itemId: string; @@ -89,12 +85,7 @@ export interface IOdspErrorAugmentations { } // @alpha -export interface IOdspOpenRequest { - // (undocumented) - codeHint?: { - containerPackageName?: string; - }; - dataStorePath?: string; +export interface IOdspOpenArgs { driveId: string; fileVersion: string | undefined; isClpCompliantApp?: boolean; diff --git a/packages/drivers/odsp-driver-definitions/src/index.ts b/packages/drivers/odsp-driver-definitions/src/index.ts index 97c4798c674a..407c539b49ac 100644 --- a/packages/drivers/odsp-driver-definitions/src/index.ts +++ b/packages/drivers/odsp-driver-definitions/src/index.ts @@ -23,8 +23,8 @@ export { } from "./odspCache.js"; export { IOdspResolvedUrl, - IOdspOpenRequest, - IOdspCreateRequest, + 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 df7233f192cc..94154ab05df4 100644 --- a/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts +++ b/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts @@ -198,7 +198,7 @@ export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { * @legacy * @alpha */ -export interface IOdspOpenRequest { +export interface IOdspOpenArgs { /** * {@inheritDoc (IOdspUrlParts:interface).siteUrl} */ @@ -233,18 +233,6 @@ export interface IOdspOpenRequest { * {@inheritDoc (ShareLinkInfoType:interface).sharingLinkToRedeem} */ sharingLinkToRedeem?: string; - - /** - * {@inheritDoc (IOdspResolvedUrl:interface).dataStorePath} - */ - dataStorePath?: string; - - /** - * {@inheritDoc (IOdspResolvedUrl:interface).codeHint} - */ - codeHint?: { - containerPackageName?: string; - }; } /** @@ -252,7 +240,7 @@ export interface IOdspOpenRequest { * @legacy * @alpha */ -export type IOdspCreateRequest = { +export type IOdspCreateArgs = { /** * {@inheritDoc (IOdspUrlParts:interface).siteUrl} */ @@ -263,18 +251,6 @@ export type IOdspCreateRequest = { */ driveId: string; - /** - * {@inheritDoc (IOdspResolvedUrl:interface).dataStorePath} - */ - dataStorePath?: string; - - /** - * {@inheritDoc (IOdspResolvedUrl:interface).codeHint} - */ - codeHint?: { - containerPackageName?: string; - }; - /** * {@inheritDoc (IOdspResolvedUrl:interface).isClpCompliantApp} */ diff --git a/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts b/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts index 57a4a209c1e2..ff992feb03f4 100644 --- a/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts +++ b/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts @@ -14,8 +14,8 @@ import { import { NonRetryableError } from "@fluidframework/driver-utils/internal"; import { IOdspResolvedUrl, - IOdspOpenRequest, - IOdspCreateRequest, + IOdspOpenArgs, + IOdspCreateArgs, OdspErrorTypes, SharingLinkRole, SharingLinkScope, @@ -105,15 +105,25 @@ export function removeNavParam(link: string): string { } /** - * Creates IOdspResolvedUrl from IOdspOpenRequest - * @param input - IOdspOpenRequest + * 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: IOdspOpenRequest, + input: IOdspOpenArgs, ): Promise { - const { siteUrl, driveId, itemId, fileVersion, dataStorePath = "", codeHint } = input; + const { siteUrl, driveId, itemId, fileVersion } = input; const hashedDocumentId = await getHashedDocumentId(driveId, itemId); assert(!hashedDocumentId.includes("/"), 0x0a8 /* "Docid should not contain slashes!!" */); @@ -133,6 +143,8 @@ export async function createOpenOdspResolvedUrl( deltaStorageUrl: getDeltaStorageUrl(siteUrl, driveId, itemId, fileVersion), }; + const dataStorePath = ""; + return { ...input, @@ -147,7 +159,7 @@ export async function createOpenOdspResolvedUrl( // only used in create-new flows fileName: "", - url: `https://placeholder/placeholder/${hashedDocumentId}/${removeBeginningSlash(dataStorePath)}`, + url: getUrlForOdspResolvedUrl(hashedDocumentId, dataStorePath), endpoints, @@ -156,28 +168,23 @@ export async function createOpenOdspResolvedUrl( }, dataStorePath, - codeHint, }; } /** - * Creates IOdspResolvedUrl from IOdspOpenRequest - * @param input - IOdspOpenRequest + * Creates IOdspResolvedUrl from IOdspOpenArgs + * @param input - IOdspOpenArgs * @returns IOdspResolvedUrl * @alpha */ -export async function createCreateOdspResolvedUrl( - input: IOdspCreateRequest, -): Promise { +export function createCreateOdspResolvedUrl(input: IOdspCreateArgs): IOdspResolvedUrl { const { siteUrl, - dataStorePath, - codeHint, itemId = "", fileName = "", filePath = "", createShareLinkType, - } = input as IOdspCreateRequest & { + } = input as IOdspCreateArgs & { createShareLinkType?: ISharingLinkKind; } & ( | { @@ -228,9 +235,6 @@ export async function createCreateOdspResolvedUrl( createKind: createShareLinkType, }, }, - - dataStorePath, - codeHint, }; } @@ -268,34 +272,48 @@ export class OdspDriverUrlResolver implements IUrlResolver { const createKind = getSharingLinkParams(searchParams); - return createCreateOdspResolvedUrl({ + const createResolvedUrl = createCreateOdspResolvedUrl({ siteUrl: siteURL, driveId: driveID, filePath, fileName, - codeHint: { - containerPackageName: packageName ?? undefined, - }, createShareLinkType: createKind, isClpCompliantApp: request.headers?.[ClpCompliantAppHeader.isClpCompliantApp], }); + return { + ...createResolvedUrl, + codeHint: { + containerPackageName: packageName ?? undefined, + }, + }; } - const { siteUrl, driveId, itemId, path, containerPackageName, fileVersion } = - decodeOdspUrl(request.url); + const { + siteUrl, + driveId, + itemId, + path: dataStorePath, + containerPackageName, + fileVersion, + } = decodeOdspUrl(request.url); const summarizer = !!request.headers?.[DriverHeader.summarizingClient]; - return createOpenOdspResolvedUrl({ + const openResolvedUrl = await createOpenOdspResolvedUrl({ siteUrl, driveId, itemId, - dataStorePath: path, summarizer, - codeHint: { - containerPackageName, - }, fileVersion, isClpCompliantApp: request.headers?.[ClpCompliantAppHeader.isClpCompliantApp], }); + + return { + ...openResolvedUrl, + dataStorePath, + url: getUrlForOdspResolvedUrl(openResolvedUrl.hashedDocumentId, dataStorePath), + codeHint: { + containerPackageName, + }, + }; } /** 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 876bf5b13cd9..741e89a1c5d5 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 @@ -329,8 +329,8 @@ export type IEventTransformer = TEvent extends { } ? TransformedEvent : TransformedEvent; // @public @sealed -export interface IFluidContainer Promise> extends IEventProvider { - attach: TAttachType; +export interface IFluidContainer extends IEventProvider { + attach(param?: 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 df15297ec69a..306a4419e6eb 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 @@ -367,8 +367,8 @@ export type IEventTransformer = TEvent extends { } ? TransformedEvent : TransformedEvent; // @public @sealed -export interface IFluidContainer Promise> extends IEventProvider { - attach: TAttachType; +export interface IFluidContainer extends IEventProvider { + attach(param?: 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 3dee853c4eb8..202be8c0f045 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 @@ -357,8 +357,8 @@ export type IEventTransformer = TEvent extends { } ? TransformedEvent : TransformedEvent; // @public @sealed -export interface IFluidContainer Promise> extends IEventProvider { - attach: TAttachType; +export interface IFluidContainer extends IEventProvider { + attach(param?: 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 8cbafd7f170c..cb13e7168e54 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 @@ -329,8 +329,8 @@ export type IEventTransformer = TEvent extends { } ? TransformedEvent : TransformedEvent; // @public @sealed -export interface IFluidContainer Promise> extends IEventProvider { - attach: TAttachType; +export interface IFluidContainer extends IEventProvider { + attach(param?: unknown): Promise; readonly attachState: AttachState; connect(): void; readonly connectionState: ConnectionStateType; 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 deb6e50f18f3..48cb6efa6aee 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 @@ -20,8 +20,8 @@ export interface IConnection { } // @public @sealed -export interface IFluidContainer Promise> extends IEventProvider { - attach: TAttachType; +export interface IFluidContainer extends IEventProvider { + attach(param?: 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 a485121ba637..2b0df2543638 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 @@ -20,8 +20,8 @@ export interface IConnection { } // @public @sealed -export interface IFluidContainer Promise> extends IEventProvider { - attach: TAttachType; +export interface IFluidContainer extends IEventProvider { + attach(param?: 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 9d570e6eec5e..71f3b3141ece 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 @@ -20,8 +20,8 @@ export interface IConnection { } // @public @sealed -export interface IFluidContainer Promise> extends IEventProvider { - attach: TAttachType; +export interface IFluidContainer extends IEventProvider { + attach(param?: unknown): Promise; readonly attachState: AttachState; connect(): void; readonly connectionState: ConnectionState; diff --git a/packages/framework/fluid-static/src/fluidContainer.ts b/packages/framework/fluid-static/src/fluidContainer.ts index 4b6a219fb5a0..0d67d45799a4 100644 --- a/packages/framework/fluid-static/src/fluidContainer.ts +++ b/packages/framework/fluid-static/src/fluidContainer.ts @@ -106,10 +106,8 @@ export interface IFluidContainerEvents extends IEvent { * @sealed * @public */ -export interface IFluidContainer< - TContainerSchema extends ContainerSchema = ContainerSchema, - TAttachType = () => Promise, -> extends IEventProvider { +export interface IFluidContainer + extends IEventProvider { /** * Provides the current connected state of the container */ @@ -172,7 +170,7 @@ export interface IFluidContainer< * * @returns A promise which resolves when the attach is complete, with the string identifier of the container. */ - attach: TAttachType; + attach(param?: unknown): Promise; /** * Attempts to connect the container to the delta stream and process operations. @@ -240,15 +238,11 @@ export interface IFluidContainer< export function createFluidContainer< TContainerSchema extends ContainerSchema, // eslint-disable-next-line @typescript-eslint/no-explicit-any - TAttachType extends (...args: any[]) => unknown, >(props: { container: IContainer; rootDataObject: IRootDataObject; -}): IFluidContainer { - return new FluidContainer( - props.container, - props.rootDataObject, - ); +}): IFluidContainer { + return new FluidContainer(props.container, props.rootDataObject); } /** @@ -260,13 +254,9 @@ export function createFluidContainer< * Note: this implementation is not complete. Consumers who rely on {@link IFluidContainer.attach} * will need to utilize or provide a service-specific implementation of this type that implements that method. */ -class FluidContainer< - TContainerSchema extends ContainerSchema, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - TAttachType extends (...args: any[]) => unknown, - > +class FluidContainer extends TypedEventEmitter - implements IFluidContainer + implements IFluidContainer { private readonly connectedHandler = (): boolean => this.emit("connected"); private readonly disconnectedHandler = (): boolean => this.emit("disconnected"); @@ -334,12 +324,12 @@ 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 readonly attach = ((...args: Parameters): ReturnType => { + public async attach(param?: unknown): Promise { if (this.container.attachState !== AttachState.Detached) { throw new Error("Cannot attach container. Container is not in detached state."); } throw new Error("Cannot attach container. Attach method not provided."); - }) as TAttachType; + } /** * {@inheritDoc IFluidContainer.connect} diff --git a/packages/service-clients/azure-client/src/AzureClient.ts b/packages/service-clients/azure-client/src/AzureClient.ts index 09fd347806b7..4a61a56b4855 100644 --- a/packages/service-clients/azure-client/src/AzureClient.ts +++ b/packages/service-clients/azure-client/src/AzureClient.ts @@ -174,7 +174,7 @@ export class AzureClient { url.searchParams.append("containerId", encodeURIComponent(id)); const container = await loader.resolve({ url: url.href }); const rootDataObject = await this.getContainerEntryPoint(container); - const fluidContainer = createFluidContainer Promise>({ + const fluidContainer = createFluidContainer({ container, rootDataObject, }); @@ -213,7 +213,7 @@ export class AzureClient { headers: { [LoaderHeader.version]: version.id }, }); const rootDataObject = await this.getContainerEntryPoint(container); - const fluidContainer = createFluidContainer Promise>({ + const fluidContainer = createFluidContainer({ container, rootDataObject, }); @@ -322,7 +322,7 @@ export class AzureClient { } return container.resolvedUrl.id; }; - const fluidContainer = createFluidContainer Promise>({ + const fluidContainer = createFluidContainer({ container, rootDataObject, }); 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 dc7e5c62d69f..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 @@ -46,8 +46,7 @@ describe("Fluid audience", () => { */ it("can find original member", async () => { const { container, services } = await client.createContainer(schema); - const res = await container.attach(); - const itemId = res.itemId; + const itemId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -81,8 +80,7 @@ describe("Fluid audience", () => { */ it.skip("can find partner member", async () => { const { container, services } = await client.createContainer(schema); - const res = await container.attach(); - const itemId = res.itemId; + const itemId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -136,8 +134,7 @@ describe("Fluid audience", () => { */ it.skip("can observe member leaving", async () => { const { container } = await client.createContainer(schema); - const res = await container.attach(); - const itemId = res.itemId; + const itemId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { 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 1f74f149a232..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 @@ -50,8 +50,7 @@ describe("Container create scenarios", () => { ); // Make sure we can attach. - const res = await container.attach(); - const itemId = res.itemId; + const itemId = await container.attach(); assert.strictEqual(typeof itemId, "string", "Attach did not return a string ID"); }); @@ -63,8 +62,7 @@ describe("Container create scenarios", () => { */ it("can attach a container", async () => { const { container } = await client.createContainer(schema); - const res = await container.attach(); - const itemId = res.itemId; + const itemId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -89,8 +87,7 @@ describe("Container create scenarios", () => { */ it("cannot attach a container twice", async () => { const { container } = await client.createContainer(schema); - const res = await container.attach(); - const itemId = res.itemId; + const itemId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -116,8 +113,7 @@ describe("Container create scenarios", () => { */ it("can retrieve existing ODSP container successfully", async () => { const { container: newContainer } = await client.createContainer(schema); - const res = await newContainer.attach(); - const itemId = res.itemId; + const itemId = await newContainer.attach(); if (newContainer.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => newContainer.once("connected", () => resolve()), { 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 40383b0677b5..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 @@ -44,8 +44,7 @@ describe("Fluid data updates", () => { */ it("can set DDSes as initial objects for a container", async () => { const { container: newContainer } = await client.createContainer(schema); - const res = await newContainer.attach(); - const itemId = res.itemId; + const itemId = await newContainer.attach(); if (newContainer.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => newContainer.once("connected", () => resolve()), { @@ -76,8 +75,7 @@ describe("Fluid data updates", () => { */ it("can change DDSes within initialObjects value", async () => { const { container } = await client.createContainer(schema); - const res = await container.attach(); - const itemId = res.itemId; + const itemId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -110,8 +108,7 @@ describe("Fluid data updates", () => { }, }; const { container } = await client.createContainer(doSchema); - const res = await container.attach(); - const itemId = res.itemId; + const itemId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -158,8 +155,7 @@ describe("Fluid data updates", () => { }, }; const { container } = await client.createContainer(doSchema); - const res = await container.attach(); - const itemId = res.itemId; + const itemId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { @@ -219,8 +215,7 @@ describe("Fluid data updates", () => { assert.strictEqual(mdo2.value, 3); - const res = await container.attach(); - const itemId = res.itemId; + const itemId = await container.attach(); if (container.connectionState !== ConnectionState.Connected) { await timeoutPromise((resolve) => container.once("connected", () => resolve()), { 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 4cb46d282815..f895c3dea666 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 @@ -13,18 +13,16 @@ export type IOdspAudience = IServiceAudience; // @alpha export interface IOdspClient { createContainer(containerSchema: T): Promise<{ - container: IOdspFluidContainer; + container: IFluidContainer; services: OdspContainerServices; + createFn: OdspContainerAttachFunctor; }>; getContainer(itemId: string, containerSchema: T, options?: OdspContainerOpenOptions): Promise<{ - container: IOdspFluidContainer; + container: IFluidContainer; services: OdspContainerServices; }>; } -// @alpha -export type IOdspFluidContainer = IFluidContainer; - // @beta export interface IOdspTokenProvider { fetchStorageToken(siteUrl: string, refresh: boolean): Promise; @@ -49,7 +47,7 @@ export interface OdspConnectionConfig { } // @alpha -export type OdspContainerAttachRequest = { +export type OdspContainerAttachArgs = { filePath?: string; fileName?: string; createShareLinkType?: ISharingLinkKind; @@ -57,15 +55,15 @@ export type OdspContainerAttachRequest = { itemId: string; }; +// @alpha +export type OdspContainerAttachFunctor = (param?: OdspContainerAttachArgs) => Promise; + // @alpha export interface OdspContainerAttachResult { itemId: string; shareLinkInfo?: ShareLinkInfoType; } -// @alpha -export type OdspContainerAttachType = (param?: OdspContainerAttachRequest) => Promise; - // @alpha export interface OdspContainerOpenOptions { fileVersion?: string; diff --git a/packages/service-clients/odsp-client/src/index.ts b/packages/service-clients/odsp-client/src/index.ts index e2acdec9cc2e..1f7eb75abc3b 100644 --- a/packages/service-clients/odsp-client/src/index.ts +++ b/packages/service-clients/odsp-client/src/index.ts @@ -20,12 +20,11 @@ export type { IOdspAudience, OdspMember, TokenResponse, - OdspContainerAttachRequest, - OdspContainerAttachType, + OdspContainerAttachArgs, + OdspContainerAttachFunctor, OdspContainerAttachResult, OdspContainerOpenOptions, IOdspClient, - IOdspFluidContainer, } from "./interfaces.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 145369e6f2bc..114299a3eff6 100644 --- a/packages/service-clients/odsp-client/src/interfaces.ts +++ b/packages/service-clients/odsp-client/src/interfaces.ts @@ -82,12 +82,12 @@ export interface OdspClientProps { } /** - * Argument type of IFluidContainer.attach() for containers created by IOdspClient * 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 type OdspContainerAttachRequest = +export type OdspContainerAttachArgs = | { /** * The file path where Fluid containers are created. If undefined, the file is created at the root. @@ -114,7 +114,8 @@ export type OdspContainerAttachRequest = }; /** - * An object type returned by IOdspFluidContainer.attach() call. * + * An object type returned by attach call. + * Please see {@link OdspContainerAttachFunctor} for more details * @alpha */ export interface OdspContainerAttachResult { @@ -124,21 +125,22 @@ export interface OdspContainerAttachResult { itemId: string; /** - * If OdspContainerAttachRequest.createShareLinkType was provided at the time of IOdspFluidContainer.attach() call, - * this value will contain sharing link information for created file. + * If OdspContainerAttachArgs.createShareLinkType was provided as part of OdspContainerAttachArgs payload, + * `shareLinkInfo` will contain sharing link information for created file. */ shareLinkInfo?: ShareLinkInfoType; } /** - * IFluidContainer.attach() function signature for IOdspClient + * 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 OdspContainerAttachType = ( - param?: OdspContainerAttachRequest, +export type OdspContainerAttachFunctor = ( + param?: OdspContainerAttachArgs, ) => Promise; /** @@ -148,7 +150,7 @@ export type OdspContainerAttachType = ( 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, result of calling IOdspFluidContainer. + * 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. * @@ -223,13 +225,6 @@ export interface TokenResponse { fromCache?: boolean; } -/** - * Fluid Container type - * @alpha - */ -export type IOdspFluidContainer = - IFluidContainer; - /** * IOdspClient provides the ability to manipulate Fluid containers backed by the ODSP service within the context of Microsoft 365 (M365) tenants. * @alpha @@ -242,8 +237,9 @@ export interface IOdspClient { createContainer( containerSchema: T, ): Promise<{ - container: IOdspFluidContainer; + container: IFluidContainer; services: OdspContainerServices; + createFn: OdspContainerAttachFunctor; }>; /** @@ -258,7 +254,7 @@ export interface IOdspClient { containerSchema: T, options?: OdspContainerOpenOptions, ): Promise<{ - container: IOdspFluidContainer; + 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 686e5b822616..fd8877eef8b5 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -22,7 +22,7 @@ import type { IUrlResolver, IResolvedUrl, } from "@fluidframework/driver-definitions/internal"; -import type { ContainerSchema } from "@fluidframework/fluid-static"; +import type { ContainerSchema, IFluidContainer } from "@fluidframework/fluid-static"; import type { IRootDataObject } from "@fluidframework/fluid-static/internal"; import { createDOProviderContainerRuntimeFactory, @@ -37,8 +37,8 @@ import { } from "@fluidframework/odsp-driver/internal"; import type { OdspResourceTokenFetchOptions, - IOdspOpenRequest, - IOdspCreateRequest, + IOdspOpenArgs, + IOdspCreateArgs, } from "@fluidframework/odsp-driver-definitions/internal"; import { wrapConfigProviderWithDefaults } from "@fluidframework/telemetry-utils/internal"; import { v4 as uuid } from "uuid"; @@ -46,14 +46,13 @@ import { v4 as uuid } from "uuid"; import type { TokenResponse, OdspClientProps, - OdspContainerAttachRequest, - OdspContainerAttachType, + OdspContainerAttachArgs, + OdspContainerAttachFunctor, OdspContainerServices, OdspContainerAttachResult, OdspContainerOpenOptions, OdspConnectionConfig, IOdspClient, - IOdspFluidContainer, } from "./interfaces.js"; import { createOdspAudienceMember } from "./odspAudience.js"; import { type IOdspTokenProvider } from "./token.js"; @@ -92,7 +91,7 @@ function wrapConfigProvider(baseConfigProvider?: IConfigProviderBase): IConfigPr } class OdspFileOpenUrlResolver implements IUrlResolver { - public constructor(private readonly input: IOdspOpenRequest) {} + public constructor(private readonly input: IOdspOpenArgs) {} public async resolve(_request: IRequest): Promise { return createOpenOdspResolvedUrl(this.input); @@ -104,11 +103,11 @@ class OdspFileOpenUrlResolver implements IUrlResolver { } class OdspFileCreateUrlResolver implements IUrlResolver { - private input?: IOdspCreateRequest; + private input?: IOdspCreateArgs; public constructor() {} - public update(input: IOdspCreateRequest): void { + public update(input: IOdspCreateArgs): void { assert(this.input === undefined, "Can update only once"); this.input = input; } @@ -178,8 +177,9 @@ class OdspClient implements IOdspClient { public async createContainer( containerSchema: T, ): Promise<{ - container: IOdspFluidContainer; + container: IFluidContainer; services: OdspContainerServices; + createFn: OdspContainerAttachFunctor; }> { const resolver = new OdspFileCreateUrlResolver(); const loader = this.createLoader(containerSchema, resolver); @@ -190,16 +190,25 @@ class OdspClient implements IOdspClient { }); const rootDataObject = await this.getContainerEntryPoint(container); - const fluidContainer = createFluidContainer({ + const fluidContainer = createFluidContainer({ container, rootDataObject, - }) as IOdspFluidContainer; + }); + + const createFn = OdspClient.createContainerAttachCallback( + container, + this.connectionConfig, + resolver, + ); - OdspClient.addAttachCallback(container, fluidContainer, this.connectionConfig, resolver); + fluidContainer.attach = async (): Promise => { + const res = await createFn(); + return res.itemId; + }; const services = await this.getContainerServices(container); - return { container: fluidContainer, services }; + return { container: fluidContainer, services, createFn }; } public async getContainer( @@ -207,10 +216,10 @@ class OdspClient implements IOdspClient { containerSchema: T, options?: OdspContainerOpenOptions, ): Promise<{ - container: IOdspFluidContainer; + container: IFluidContainer; services: OdspContainerServices; }> { - const resolvedUrl: IOdspOpenRequest = { + const resolvedUrl: IOdspOpenArgs = { summarizer: false, // Identity of a file @@ -233,7 +242,7 @@ class OdspClient implements IOdspClient { // Put some easily editifiable string for easier debugging const container = await loader.resolve({ url: "" }); - const fluidContainer = createFluidContainer({ + const fluidContainer = createFluidContainer({ container, rootDataObject: await this.getContainerEntryPoint(container), }); @@ -274,18 +283,15 @@ class OdspClient implements IOdspClient { }); } - private static addAttachCallback( + private static createContainerAttachCallback( container: IContainer, - fluidContainer: IOdspFluidContainer, connectionConfig: OdspSiteLocation, resolver: OdspFileCreateUrlResolver, - ): void { + ): OdspContainerAttachFunctor { /** * See {@link FluidContainer.attach} */ - fluidContainer.attach = async ( - odspProps?: OdspContainerAttachRequest, - ): Promise => { + return async (odspProps?: OdspContainerAttachArgs): Promise => { if (container.attachState !== AttachState.Detached) { throw new Error("Cannot attach container. Container is not in detached state"); } @@ -296,7 +302,7 @@ class OdspClient implements IOdspClient { isClpCompliantApp: connectionConfig.isClpCompliant === true, }; - const resolved: IOdspCreateRequest = + const resolved: IOdspCreateArgs = odspProps !== undefined && "itemId" in odspProps ? { ...base, diff --git a/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts b/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts index 5305f2aaca3f..a7de506f19d9 100644 --- a/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts +++ b/packages/service-clients/tinylicious-client/src/TinyliciousClient.ts @@ -112,7 +112,7 @@ export class TinyliciousClient { return container.resolvedUrl.id; }; - const fluidContainer = createFluidContainer Promise>({ + const fluidContainer = createFluidContainer({ container, rootDataObject, }); @@ -140,7 +140,7 @@ export class TinyliciousClient { const loader = this.createLoader(containerSchema, compatibilityMode); const container = await loader.resolve({ url: id }); const rootDataObject = await this.getContainerEntryPoint(container); - const fluidContainer = createFluidContainer Promise>({ + const fluidContainer = createFluidContainer({ container, rootDataObject, }); 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 From af752a7136ff5dc06e4335f315f9ec3ebaac5581 Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Thu, 8 Aug 2024 16:41:17 -0700 Subject: [PATCH 10/10] Some code cleanup --- .../odsp-client/shared-tree-demo/src/reactApp.tsx | 2 +- .../src/odspDocumentServiceFactoryCore.ts | 2 +- .../odsp-driver/src/odspDriverUrlResolver.ts | 1 - .../api-report/fluid-framework.beta.api.md | 2 +- .../fluid-framework.legacy.alpha.api.md | 2 +- .../fluid-framework.legacy.public.api.md | 2 +- .../api-report/fluid-framework.public.api.md | 2 +- .../api-report/fluid-static.alpha.api.md | 2 +- .../api-report/fluid-static.beta.api.md | 2 +- .../api-report/fluid-static.public.api.md | 2 +- .../framework/fluid-static/src/fluidContainer.ts | 7 +++---- packages/service-clients/odsp-client/README.md | 4 ++-- .../api-report/odsp-client.alpha.api.md | 6 +++--- .../api-report/odsp-client.beta.api.md | 15 +++++++++++++++ .../service-clients/odsp-client/src/interfaces.ts | 6 +++--- 15 files changed, 35 insertions(+), 22 deletions(-) diff --git a/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx b/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx index ec343f9381f3..4279201403d2 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx +++ b/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx @@ -5,7 +5,7 @@ /* eslint-disable prefer-template */ -import { Tree, TreeView, IFluidContainer } from "fluid-framework"; +import { IFluidContainer, Tree, TreeView } from "fluid-framework"; import React, { ReactNode, useEffect, useState } from "react"; import { App, Letter } from "./schema.js"; diff --git a/packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts b/packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts index 666e7000696d..84c6ef4256bb 100644 --- a/packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts +++ b/packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts @@ -21,11 +21,11 @@ import { IOdspUrlParts, IPersistedCache, IRelaySessionAwareDriverFactory, + ISharingLinkKind, ISocketStorageDiscovery, OdspResourceTokenFetchOptions, TokenFetchOptions, TokenFetcher, - ISharingLinkKind, } from "@fluidframework/odsp-driver-definitions/internal"; import { PerformanceEvent, createChildLogger } from "@fluidframework/telemetry-utils/internal"; import { v4 as uuid } from "uuid"; diff --git a/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts b/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts index ff992feb03f4..8ae1cd25bc6e 100644 --- a/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts +++ b/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts @@ -217,7 +217,6 @@ export function createCreateOdspResolvedUrl(input: IOdspCreateArgs): IOdspResolv filePath, fileName, - // Is url even used? url: `https://${siteUrl}?&version=null`, endpoints: { 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 741e89a1c5d5..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 @@ -330,7 +330,7 @@ export type IEventTransformer = TEvent extends { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(param?: unknown): 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 306a4419e6eb..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 @@ -368,7 +368,7 @@ export type IEventTransformer = TEvent extends { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(param?: unknown): 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 202be8c0f045..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 @@ -358,7 +358,7 @@ export type IEventTransformer = TEvent extends { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(param?: unknown): 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 cb13e7168e54..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 @@ -330,7 +330,7 @@ export type IEventTransformer = TEvent extends { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(param?: unknown): Promise; + attach(props?: unknown): Promise; readonly attachState: AttachState; connect(): void; readonly connectionState: ConnectionStateType; 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 48cb6efa6aee..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 @@ -21,7 +21,7 @@ export interface IConnection { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(param?: unknown): 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 2b0df2543638..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 @@ -21,7 +21,7 @@ export interface IConnection { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(param?: unknown): 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 71f3b3141ece..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 @@ -21,7 +21,7 @@ export interface IConnection { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(param?: unknown): Promise; + attach(props?: unknown): Promise; readonly attachState: AttachState; connect(): void; readonly connectionState: ConnectionState; diff --git a/packages/framework/fluid-static/src/fluidContainer.ts b/packages/framework/fluid-static/src/fluidContainer.ts index 0d67d45799a4..b1a10e347d3b 100644 --- a/packages/framework/fluid-static/src/fluidContainer.ts +++ b/packages/framework/fluid-static/src/fluidContainer.ts @@ -170,7 +170,7 @@ export interface IFluidContainer; + attach(props?: unknown): Promise; /** * Attempts to connect the container to the delta stream and process operations. @@ -236,8 +236,7 @@ export interface IFluidContainer(props: { container: IContainer; rootDataObject: IRootDataObject; @@ -254,7 +253,7 @@ export function createFluidContainer< * Note: this implementation is not complete. Consumers who rely on {@link IFluidContainer.attach} * will need to utilize or provide a service-specific implementation of this type that implements that method. */ -class FluidContainer +class FluidContainer extends TypedEventEmitter implements IFluidContainer { diff --git a/packages/service-clients/odsp-client/README.md b/packages/service-clients/odsp-client/README.md index 0a96928a7437..b8eee4ac851a 100644 --- a/packages/service-clients/odsp-client/README.md +++ b/packages/service-clients/odsp-client/README.md @@ -104,9 +104,9 @@ 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 container.attach(); +const response = await createFn(); const itemId = response.itemId; ``` 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 f895c3dea666..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 @@ -7,7 +7,7 @@ // @alpha export function createOdspClient(properties: OdspClientProps): IOdspClient; -// @alpha +// @beta export type IOdspAudience = IServiceAudience; // @alpha @@ -70,12 +70,12 @@ export interface OdspContainerOpenOptions { sharingLinkToRedeem?: string; } -// @alpha +// @beta export interface OdspContainerServices { audience: IOdspAudience; } -// @alpha +// @beta export interface OdspMember extends IMember { email: string; id: string; 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 e7e1aeac7d85..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 @@ -4,6 +4,9 @@ ```ts +// @beta +export type IOdspAudience = IServiceAudience; + // @beta export interface IOdspTokenProvider { fetchStorageToken(siteUrl: string, refresh: boolean): Promise; @@ -18,6 +21,18 @@ export interface OdspConnectionConfig { tokenProvider: IOdspTokenProvider; } +// @beta +export interface OdspContainerServices { + audience: IOdspAudience; +} + +// @beta +export interface OdspMember extends IMember { + email: string; + id: string; + name: string; +} + // @beta export interface TokenResponse { fromCache?: boolean; diff --git a/packages/service-clients/odsp-client/src/interfaces.ts b/packages/service-clients/odsp-client/src/interfaces.ts index 114299a3eff6..60a7b672fd38 100644 --- a/packages/service-clients/odsp-client/src/interfaces.ts +++ b/packages/service-clients/odsp-client/src/interfaces.ts @@ -172,7 +172,7 @@ export interface OdspContainerOpenOptions { * FluidContainer is persisted in the backend and consumed by users. Any functionality regarding * how the data is handled within the FluidContainer itself, i.e. which data objects or DDSes to * use, will not be included here but rather on the FluidContainer class itself. - * @alpha + * @beta */ export interface OdspContainerServices { /** @@ -185,7 +185,7 @@ export interface OdspContainerServices { * Since ODSP provides user names and email for all of its members, we extend the * {@link @fluidframework/fluid-static#IMember} interface to include this service-specific value. * It will be returned for all audience members connected. - * @alpha + * @beta */ export interface OdspMember extends IMember { /** @@ -204,7 +204,7 @@ export interface OdspMember extends IMember { /** * Audience object for ODSP containers - * @alpha + * @beta */ export type IOdspAudience = IServiceAudience;