From 4ce9dcc0243cd881e8e71e5d861d230c1ab7e2d8 Mon Sep 17 00:00:00 2001 From: Garrett Ausfeldt Date: Thu, 12 Nov 2020 13:33:37 -0800 Subject: [PATCH] Add analytical store schema POC (#164) * add schema APIs to JunoClient * start implementing buildSchemaNode * finish getSchemaNodes * finish implementing addSchema * cleanup * make schema optional * handle undefined/null schema and fields. Also don't retry on gettting schema failures. * fix request schema and get schema endpoints * add feature flag * try to get most recent schema when refreshed or initialized. * add tests * cleanup * cleanup * cleanup * fix merge conflict typos * fix lint errors * fix tests and update snapshot Co-authored-by: REDMOND\gaausfel --- src/Common/Constants.ts | 1 + src/Contracts/DataModels.ts | 34 +++ src/Contracts/ViewModels.ts | 3 + .../SettingsComponent.test.tsx.snap | 4 + src/Explorer/Explorer.ts | 2 + src/Explorer/Tree/Collection.ts | 4 + src/Explorer/Tree/Database.test.ts | 82 ++++++ src/Explorer/Tree/Database.ts | 46 ++++ .../Tree/ResourceTreeAdapter.test.tsx | 253 ++++++++++++++++++ src/Explorer/Tree/ResourceTreeAdapter.tsx | 77 +++++- .../ResourceTreeAdapter.test.tsx.snap | 172 ++++++++++++ src/Juno/JunoClient.ts | 50 ++++ 12 files changed, 727 insertions(+), 1 deletion(-) create mode 100644 src/Explorer/Tree/Database.test.ts create mode 100644 src/Explorer/Tree/ResourceTreeAdapter.test.tsx create mode 100644 src/Explorer/Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 32b629149..2142a530e 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -125,6 +125,7 @@ export class Features { public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput"; public static readonly ttl90Days = "ttl90days"; public static readonly enableRightPanelV2 = "enablerightpanelv2"; + public static readonly enableSchema = "enableschema"; public static readonly enableSDKoperations = "enablesdkoperations"; public static readonly showMinRUSurvey = "showminrusurvey"; } diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 3aadf14cd..82779e428 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -88,6 +88,38 @@ export interface Resource { id: string; } +export interface IType { + name: string; + code: number; +} + +export interface IDataField { + dataType: IType; + hasNulls: boolean; + isArray: boolean; + schemaType: IType; + name: string; + path: string; + maxRepetitionLevel: number; + maxDefinitionLevel: number; +} + +export interface ISchema { + id: string; + accountName: string; + resource: string; + fields: IDataField[]; +} + +export interface ISchemaRequest { + id: string; + subscriptionId: string; + resourceGroup: string; + accountName: string; + resource: string; + status: string; +} + export interface Collection extends Resource { defaultTtl?: number; indexingPolicy?: IndexingPolicy; @@ -98,6 +130,8 @@ export interface Collection extends Resource { changeFeedPolicy?: ChangeFeedPolicy; analyticalStorageTtl?: number; geospatialConfig?: GeospatialConfig; + schema?: ISchema; + requestSchema?: () => void; } export interface Database extends Resource { diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 94f6ac2ec..f0a0e45e5 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -116,6 +116,8 @@ export interface CollectionBase extends TreeNode { export interface Collection extends CollectionBase { defaultTtl: ko.Observable; analyticalStorageTtl: ko.Observable; + schema?: DataModels.ISchema; + requestSchema?: () => void; indexingPolicy: ko.Observable; uniqueKeyPolicy: DataModels.UniqueKeyPolicy; quotaInfo: ko.Observable; @@ -359,6 +361,7 @@ export enum CollectionTabKind { SparkMasterTab = 16, Gallery = 17, NotebookViewer = 18, + Schema = 19, SettingsV2 = 19 } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index db28fa5a8..428808056 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -971,6 +971,7 @@ exports[`SettingsComponent renders 1`] = ` "isRefreshingExplorer": [Function], "isResourceTokenCollectionNodeSelected": [Function], "isRightPanelV2Enabled": [Function], + "isSchemaEnabled": [Function], "isServerlessEnabled": [Function], "isSettingsV2Enabled": [Function], "isSparkEnabled": [Function], @@ -2251,6 +2252,7 @@ exports[`SettingsComponent renders 1`] = ` "isRefreshingExplorer": [Function], "isResourceTokenCollectionNodeSelected": [Function], "isRightPanelV2Enabled": [Function], + "isSchemaEnabled": [Function], "isServerlessEnabled": [Function], "isSettingsV2Enabled": [Function], "isSparkEnabled": [Function], @@ -3544,6 +3546,7 @@ exports[`SettingsComponent renders 1`] = ` "isRefreshingExplorer": [Function], "isResourceTokenCollectionNodeSelected": [Function], "isRightPanelV2Enabled": [Function], + "isSchemaEnabled": [Function], "isServerlessEnabled": [Function], "isSettingsV2Enabled": [Function], "isSparkEnabled": [Function], @@ -4824,6 +4827,7 @@ exports[`SettingsComponent renders 1`] = ` "isRefreshingExplorer": [Function], "isResourceTokenCollectionNodeSelected": [Function], "isRightPanelV2Enabled": [Function], + "isSchemaEnabled": [Function], "isServerlessEnabled": [Function], "isSettingsV2Enabled": [Function], "isSparkEnabled": [Function], diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index bb7fbce2d..bef261953 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -226,6 +226,7 @@ export default class Explorer { public shareTokenCopyHelperText: ko.Observable; public shouldShowDataAccessExpiryDialog: ko.Observable; public shouldShowContextSwitchPrompt: ko.Observable; + public isSchemaEnabled: ko.Computed; // Notebooks public isNotebookEnabled: ko.Observable; @@ -421,6 +422,7 @@ export default class Explorer { this.isFeatureEnabled(Constants.Features.canExceedMaximumValue) ); + this.isSchemaEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableSchema)); this.isNotificationConsoleExpanded = ko.observable(false); this.databases = ko.observableArray(); diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index c5db74aeb..897752b92 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -63,6 +63,8 @@ export default class Collection implements ViewModels.Collection { public throughput: ko.Computed; public rawDataModel: DataModels.Collection; public analyticalStorageTtl: ko.Observable; + public schema: DataModels.ISchema; + public requestSchema: () => void; public geospatialConfig: ko.Observable; // TODO move this to API customization class @@ -117,6 +119,8 @@ export default class Collection implements ViewModels.Collection { this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy); this.changeFeedPolicy = ko.observable(data.changeFeedPolicy); this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl); + this.schema = data.schema; + this.requestSchema = data.requestSchema; this.geospatialConfig = ko.observable(data.geospatialConfig); // TODO fix this to only replace non-excaped single quotes diff --git a/src/Explorer/Tree/Database.test.ts b/src/Explorer/Tree/Database.test.ts new file mode 100644 index 000000000..a067f6747 --- /dev/null +++ b/src/Explorer/Tree/Database.test.ts @@ -0,0 +1,82 @@ +import * as DataModels from "../../Contracts/DataModels"; +import * as ko from "knockout"; +import Database from "./Database"; +import Explorer from "../Explorer"; +import { HttpStatusCodes } from "../../Common/Constants"; +import { JunoClient } from "../../Juno/JunoClient"; +import { userContext, updateUserContext } from "../../UserContext"; + +const createMockContainer = (): Explorer => { + const mockContainer = new Explorer(); + return mockContainer; +}; + +updateUserContext({ + subscriptionId: "fakeSubscriptionId", + resourceGroup: "fakeResourceGroup", + databaseAccount: { + id: "id", + name: "fakeName", + location: "fakeLocation", + type: "fakeType", + tags: undefined, + kind: "fakeKind", + properties: { + documentEndpoint: "fakeEndpoint", + tableEndpoint: "fakeEndpoint", + gremlinEndpoint: "fakeEndpoint", + cassandraEndpoint: "fakeEndpoint" + } + } +}); + +describe("Add Schema", () => { + it("should not call requestSchema or getSchema if analyticalStorageTtl is undefined", () => { + const collection: DataModels.Collection = {} as DataModels.Collection; + collection.analyticalStorageTtl = undefined; + const database = new Database(createMockContainer(), { id: "fakeId" }); + database.container = createMockContainer(); + database.container.isSchemaEnabled = ko.computed(() => false); + + database.junoClient = new JunoClient(); + database.junoClient.requestSchema = jest.fn(); + database.junoClient.getSchema = jest.fn(); + + database.addSchema(collection); + + expect(database.junoClient.requestSchema).toBeCalledTimes(0); + }); + + it("should call requestSchema or getSchema if analyticalStorageTtl is not undefined", () => { + const collection: DataModels.Collection = { id: "fakeId" } as DataModels.Collection; + collection.analyticalStorageTtl = 0; + + const database = new Database(createMockContainer(), {}); + database.container = createMockContainer(); + database.container.isSchemaEnabled = ko.computed(() => true); + + database.junoClient = new JunoClient(); + database.junoClient.requestSchema = jest.fn(); + database.junoClient.getSchema = jest.fn().mockResolvedValue({ status: HttpStatusCodes.OK, data: {} }); + + jest.useFakeTimers(); + const interval = 5000; + const checkForSchema: NodeJS.Timeout = database.addSchema(collection, interval); + jest.advanceTimersByTime(interval + 1000); + + expect(database.junoClient.requestSchema).toBeCalledWith({ + id: undefined, + subscriptionId: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + accountName: userContext.databaseAccount.name, + resource: `dbs/${database.id}/colls/${collection.id}`, + status: "new" + }); + expect(checkForSchema).not.toBeNull(); + expect(database.junoClient.getSchema).toBeCalledWith( + userContext.databaseAccount.name, + database.id(), + collection.id + ); + }); +}); diff --git a/src/Explorer/Tree/Database.ts b/src/Explorer/Tree/Database.ts index 304774bfd..cf6c48c08 100644 --- a/src/Explorer/Tree/Database.ts +++ b/src/Explorer/Tree/Database.ts @@ -13,6 +13,8 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol import * as Logger from "../../Common/Logger"; import Explorer from "../Explorer"; import { readCollections } from "../../Common/dataAccess/readCollections"; +import { JunoClient, IJunoResponse } from "../../Juno/JunoClient"; +import { userContext } from "../../UserContext"; import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { fetchPortalNotifications } from "../../Common/PortalNotifications"; @@ -29,6 +31,7 @@ export default class Database implements ViewModels.Database { public isDatabaseExpanded: ko.Observable; public isDatabaseShared: ko.Computed; public selectedSubnodeKind: ko.Observable; + public junoClient: JunoClient; constructor(container: Explorer, data: any) { this.nodeKind = "Database"; @@ -43,6 +46,7 @@ export default class Database implements ViewModels.Database { this.isDatabaseShared = ko.pureComputed(() => { return this.offer && !!this.offer(); }); + this.junoClient = new JunoClient(); } public onSettingsClick = () => { @@ -184,6 +188,10 @@ export default class Database implements ViewModels.Database { const collections: DataModels.Collection[] = await readCollections(this.id()); const deltaCollections = this.getDeltaCollections(collections); + collections.forEach((collection: DataModels.Collection) => { + this.addSchema(collection); + }); + deltaCollections.toAdd.forEach((collection: DataModels.Collection) => { const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null); collectionVMs.push(collectionVM); @@ -308,4 +316,42 @@ export default class Database implements ViewModels.Database { this.collections(collectionsToKeep); } + + public addSchema(collection: DataModels.Collection, interval?: number): NodeJS.Timeout { + let checkForSchema: NodeJS.Timeout = null; + interval = interval || 5000; + + if (collection.analyticalStorageTtl !== undefined && this.container.isSchemaEnabled()) { + collection.requestSchema = () => { + this.junoClient.requestSchema({ + id: undefined, + subscriptionId: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + accountName: userContext.databaseAccount.name, + resource: `dbs/${this.id}/colls/${collection.id}`, + status: "new" + }); + checkForSchema = setInterval(async () => { + const response: IJunoResponse = await this.junoClient.getSchema( + userContext.databaseAccount.name, + this.id(), + collection.id + ); + + if (response.status >= 404) { + clearInterval(checkForSchema); + } + + if (response.data !== null) { + clearInterval(checkForSchema); + collection.schema = response.data; + } + }, interval); + }; + + collection.requestSchema(); + } + + return checkForSchema; + } } diff --git a/src/Explorer/Tree/ResourceTreeAdapter.test.tsx b/src/Explorer/Tree/ResourceTreeAdapter.test.tsx new file mode 100644 index 000000000..790f1b3e1 --- /dev/null +++ b/src/Explorer/Tree/ResourceTreeAdapter.test.tsx @@ -0,0 +1,253 @@ +import * as ko from "knockout"; +import * as DataModels from "../../Contracts/DataModels"; +import * as ViewModels from "../../Contracts/ViewModels"; +import React from "react"; +import { ResourceTreeAdapter } from "./ResourceTreeAdapter"; +import { shallow } from "enzyme"; +import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent"; +import Explorer from "../Explorer"; +import Collection from "./Collection"; + +const schema: DataModels.ISchema = { + id: "fakeSchemaId", + accountName: "fakeAccountName", + resource: "dbs/FakeDbName/colls/FakeCollectionName", + fields: [ + { + dataType: { + code: 15, + name: "String" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "_rid", + path: "_rid", + maxRepetitionLevel: 0, + maxDefinitionLevel: 1 + }, + { + dataType: { + code: 11, + name: "Int64" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "_ts", + path: "_ts", + maxRepetitionLevel: 0, + maxDefinitionLevel: 1 + }, + { + dataType: { + code: 15, + name: "String" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "id", + path: "id", + maxRepetitionLevel: 0, + maxDefinitionLevel: 1 + }, + { + dataType: { + code: 15, + name: "String" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "pk", + path: "pk", + maxRepetitionLevel: 0, + maxDefinitionLevel: 1 + }, + { + dataType: { + code: 15, + name: "String" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "other", + path: "other", + maxRepetitionLevel: 0, + maxDefinitionLevel: 1 + }, + { + dataType: { + code: 15, + name: "String" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "name", + path: "nested.name", + maxRepetitionLevel: 0, + maxDefinitionLevel: 1 + }, + { + dataType: { + code: 11, + name: "Int64" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "someNumber", + path: "nested.someNumber", + maxRepetitionLevel: 0, + maxDefinitionLevel: 1 + }, + { + dataType: { + code: 17, + name: "Double" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "anotherNumber", + path: "nested.anotherNumber", + maxRepetitionLevel: 0, + maxDefinitionLevel: 1 + }, + { + dataType: { + code: 15, + name: "String" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "name", + path: "items.list.items.name", + maxRepetitionLevel: 1, + maxDefinitionLevel: 3 + }, + { + dataType: { + code: 11, + name: "Int64" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "someNumber", + path: "items.list.items.someNumber", + maxRepetitionLevel: 1, + maxDefinitionLevel: 3 + }, + { + dataType: { + code: 17, + name: "Double" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "anotherNumber", + path: "items.list.items.anotherNumber", + maxRepetitionLevel: 1, + maxDefinitionLevel: 3 + }, + { + dataType: { + code: 15, + name: "String" + }, + hasNulls: true, + isArray: false, + schemaType: { + code: 0, + name: "Data" + }, + name: "_etag", + path: "_etag", + maxRepetitionLevel: 0, + maxDefinitionLevel: 1 + } + ] +}; + +const createMockContainer = (): Explorer => { + const mockContainer = new Explorer(); + mockContainer.selectedNode = ko.observable(); + mockContainer.onUpdateTabsButtons = () => { + return; + }; + + return mockContainer; +}; + +const createMockCollection = (): ViewModels.Collection => { + const mockCollection = {} as DataModels.Collection; + mockCollection._rid = "fakeRid"; + mockCollection._self = "fakeSelf"; + mockCollection.id = "fakeId"; + mockCollection.analyticalStorageTtl = 0; + mockCollection.schema = schema; + + const mockCollectionVM: ViewModels.Collection = new Collection( + createMockContainer(), + "fakeDatabaseId", + mockCollection, + undefined, + undefined + ); + + return mockCollectionVM; +}; + +describe("Resource tree for schema", () => { + const mockContainer: Explorer = createMockContainer(); + const resourceTree = new ResourceTreeAdapter(mockContainer); + + it("should render", () => { + const rootNode: TreeNode = resourceTree.buildSchemaNode(createMockCollection()); + const props: TreeComponentProps = { + rootNode, + className: "dataResourceTree" + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index b5bf40448..db00e691c 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -2,7 +2,7 @@ import * as ko from "knockout"; import * as React from "react"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; -import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; +import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } from "../Controls/TreeComponent/TreeComponent"; import * as ViewModels from "../../Contracts/ViewModels"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory"; @@ -32,6 +32,7 @@ import StoredProcedure from "./StoredProcedure"; import Trigger from "./Trigger"; import TabsBase from "../Tabs/TabsBase"; import { userContext } from "../../UserContext"; +import * as DataModels from "../../Contracts/DataModels"; export class ResourceTreeAdapter implements ReactAdapter { public static readonly MyNotebooksTitle = "My Notebooks"; @@ -289,6 +290,11 @@ export class ResourceTreeAdapter implements ReactAdapter { }); } + const schemaNode: TreeNode = this.buildSchemaNode(collection); + if (schemaNode) { + children.push(schemaNode); + } + if (ResourceTreeAdapter.showScriptNodes(this.container)) { children.push(this.buildStoredProcedureNode(collection)); children.push(this.buildUserDefinedFunctionsNode(collection)); @@ -405,6 +411,75 @@ export class ResourceTreeAdapter implements ReactAdapter { }; } + public buildSchemaNode(collection: ViewModels.Collection): TreeNode { + if (collection.analyticalStorageTtl() == undefined) { + return undefined; + } + + if (!collection.schema || !collection.schema.fields) { + return undefined; + } + + return { + label: "Schema", + children: this.getSchemaNodes(collection.schema.fields), + onClick: () => { + collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema); + this.container.tabsManager.refreshActiveTab( + (tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid + ); + } + }; + } + + private getSchemaNodes(fields: DataModels.IDataField[]): TreeNode[] { + const schema: any = {}; + + //unflatten + fields.forEach((field: DataModels.IDataField, fieldIndex: number) => { + const path: string[] = field.path.split("."); + const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`]; + let current: any = {}; + path.forEach((name: string, pathIndex: number) => { + if (pathIndex === 0) { + if (schema[name] === undefined) { + if (pathIndex === path.length - 1) { + schema[name] = fieldProperties; + } else { + schema[name] = {}; + } + } + current = schema[name]; + } else { + if (current[name] === undefined) { + if (pathIndex === path.length - 1) { + current[name] = fieldProperties; + } else { + current[name] = {}; + } + } + current = current[name]; + } + }); + }); + + const traverse = (obj: any): TreeNode[] => { + const children: TreeNode[] = []; + + if (obj !== null && !Array.isArray(obj) && typeof obj === "object") { + Object.entries(obj).forEach(([key, value]) => { + children.push({ label: key, children: traverse(value) }); + }); + } else if (Array.isArray(obj)) { + return [{ label: obj[0] }, { label: obj[1] }]; + } + + return children; + }; + + return traverse(schema); + } + private buildNotebooksTrees(): TreeNode { let notebooksTree: TreeNode = { label: undefined, diff --git a/src/Explorer/Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap b/src/Explorer/Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap new file mode 100644 index 000000000..e769c8764 --- /dev/null +++ b/src/Explorer/Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap @@ -0,0 +1,172 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Resource tree for schema should render 1`] = ` +
+ +
+`; diff --git a/src/Juno/JunoClient.ts b/src/Juno/JunoClient.ts index 76db63602..b4724002d 100644 --- a/src/Juno/JunoClient.ts +++ b/src/Juno/JunoClient.ts @@ -7,6 +7,7 @@ import { IGitHubResponse } from "../GitHub/GitHubClient"; import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService"; import { userContext } from "../UserContext"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; +import { number } from "prop-types"; export interface IJunoResponse { status: number; @@ -427,6 +428,51 @@ export class JunoClient { }; } + public async requestSchema( + schemaRequest: DataModels.ISchemaRequest + ): Promise> { + const response = await window.fetch(`${this.getAnalyticsUrl()}/${schemaRequest.accountName}/schema/request`, { + method: "POST", + body: JSON.stringify(schemaRequest), + headers: JunoClient.getHeaders() + }); + + let data: DataModels.ISchemaRequest; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data + }; + } + + public async getSchema( + accountName: string, + databaseName: string, + containerName: string + ): Promise> { + const response = await window.fetch( + `${this.getAnalyticsUrl()}/${accountName}/schema/${databaseName}/${containerName}`, + { + method: "GET", + headers: JunoClient.getHeaders() + } + ); + + let data: DataModels.ISchema; + + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } + + return { + status: response.status, + data + }; + } + private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise> { const response = await window.fetch(input, init); @@ -457,6 +503,10 @@ export class JunoClient { return `${this.getNotebooksUrl()}/${this.getAccount()}`; } + private getAnalyticsUrl(): string { + return `${configContext.JUNO_ENDPOINT}/api/analytics`; + } + private static getHeaders(): HeadersInit { const authorizationHeader = getAuthorizationHeader(); return {