From af8c3e1a14f1c43212c65dd198f7d23808930c92 Mon Sep 17 00:00:00 2001 From: Alexandru Popovici Date: Fri, 26 Apr 2024 11:02:14 +0300 Subject: [PATCH] New URL formats in URLHelper (#2229) * URLHelper now supports both old and new speckle URLs. Moved URLHelper to the viewer library so users can use it to transform speckle urls in loadable object urls. Disabled object-loader from reading the local storage for auth tokens. * Added URI decoding * Error checking. URLHelper is not a class anymore * Queries are not run in parallel --- packages/objectloader/src/index.js | 17 +- packages/viewer-sandbox/src/Sandbox.ts | 4 +- packages/viewer-sandbox/src/UrlHelper.ts | 83 ------ packages/viewer-sandbox/src/main.ts | 4 +- packages/viewer/package.json | 1 + packages/viewer/src/index.ts | 2 + packages/viewer/src/modules/UrlHelper.ts | 310 +++++++++++++++++++++++ yarn.lock | 1 + 8 files changed, 330 insertions(+), 92 deletions(-) delete mode 100644 packages/viewer-sandbox/src/UrlHelper.ts create mode 100644 packages/viewer/src/modules/UrlHelper.ts diff --git a/packages/objectloader/src/index.js b/packages/objectloader/src/index.js index 5baacea7d1..1e858d6850 100644 --- a/packages/objectloader/src/index.js +++ b/packages/objectloader/src/index.js @@ -2,7 +2,6 @@ import 'core-js' import 'regenerator-runtime/runtime' -import { SafeLocalStorage } from '@speckle/shared' import { ObjectLoaderConfigurationError, ObjectLoaderRuntimeError @@ -54,11 +53,17 @@ export default class ObjectLoader { } this.logger('Object loader constructor called!') - try { - this.token = token || SafeLocalStorage.get('AuthToken') - } catch (error) { - // Accessing localStorage may throw when executing on sandboxed document, ignore. - } + + /** I don't think the object-loader should read the token from local storage, since there is no + * builtin mechanism that sets it in the first place. So you're reading a key from the local storage + * and hoping it will magically be there. + */ + // try { + // this.token = token || SafeLocalStorage.get('AuthToken') + // } catch (error) { + // // Accessing localStorage may throw when executing on sandboxed document, ignore. + // } + this.token = token this.headers = { Accept: 'text/plain' diff --git a/packages/viewer-sandbox/src/Sandbox.ts b/packages/viewer-sandbox/src/Sandbox.ts index bb6e8005a7..7caea9d479 100644 --- a/packages/viewer-sandbox/src/Sandbox.ts +++ b/packages/viewer-sandbox/src/Sandbox.ts @@ -13,10 +13,10 @@ import { ExplodeExtension, DiffExtension, SpeckleLoader, - ObjLoader + ObjLoader, + UrlHelper } from '@speckle/viewer' import { FolderApi, Pane } from 'tweakpane' -import UrlHelper from './UrlHelper' import { DiffResult } from '@speckle/viewer' import type { PipelineOptions } from '@speckle/viewer/dist/modules/pipeline/Pipeline' import { Units } from '@speckle/viewer' diff --git a/packages/viewer-sandbox/src/UrlHelper.ts b/packages/viewer-sandbox/src/UrlHelper.ts deleted file mode 100644 index b45c4484fd..0000000000 --- a/packages/viewer-sandbox/src/UrlHelper.ts +++ /dev/null @@ -1,83 +0,0 @@ -interface CommitReferencedObjectUrl { - origin: string - streamId: string - commitId: string -} - -export default class UrlHelper { - static async getResourceUrls(url: string): Promise { - const parsed = new URL(url) - const streamId = url.split('/streams/')[1].substring(0, 10) - - const objsUrls = [] - // supports commit based urls - if (url.includes('commits')) { - const commitId = url.split('/commits/')[1].substring(0, 10) - const objUrl = await this.getCommitReferencedObjectUrl({ - origin: parsed.origin, - streamId, - commitId - }) - objsUrls.push(objUrl) - } - - // object based urls - if (url.includes('objects')) objsUrls.push(url) - - // supports urls that include overlay queries - // e.g., https://speckle.xyz/streams/a632e7a784/objects/457c45feffa6f954572e5e86fb6d4f25?overlay=cf8dc76247,f5adc1d991b3dceb4b5ad6b50f919a0e - if (url.includes('overlay=')) { - const searchParams = new URLSearchParams(parsed.search) - const resIds = searchParams.get('overlay')?.split(',') - if (resIds !== undefined) { - for (const resId of resIds) { - if (resId.length === 10) { - objsUrls.push( - await this.getCommitReferencedObjectUrl({ - origin: parsed.origin, - streamId, - commitId: resId - } as CommitReferencedObjectUrl) - ) - } else { - objsUrls.push(`${parsed.origin}/streams/${streamId}/objects/${resId}`) - } - } - } - } - - return objsUrls - } - - private static async getCommitReferencedObjectUrl(ref: CommitReferencedObjectUrl) { - const headers: { 'Content-Type': string; Authorization: string } = { - 'Content-Type': 'application/json', - Authorization: '' - } - const authToken = localStorage.getItem( - ref.origin.includes('latest') ? 'AuthTokenLatest' : 'AuthToken' - ) - if (authToken) { - headers['Authorization'] = `Bearer ${authToken}` - } - const res = await fetch(`${ref.origin}/graphql`, { - method: 'POST', - headers, - body: JSON.stringify({ - query: ` - query Stream($streamId: String!, $commitId: String!) { - stream(id: $streamId) { - commit(id: $commitId) { - referencedObject - } - } - } - `, - variables: { streamId: ref.streamId, commitId: ref.commitId } - }) - }) - - const { data } = await res.json() - return `${ref.origin}/streams/${ref.streamId}/objects/${data.stream.commit.referencedObject}` - } -} diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index cc790b0966..fbaf937fce 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -20,6 +20,7 @@ import { } from '@speckle/viewer' import { SectionTool } from '@speckle/viewer' import { SectionOutlines } from '@speckle/viewer' + const createViewer = async (containerName: string, stream: string) => { const container = document.querySelector(containerName) @@ -122,6 +123,7 @@ const getStream = () => { // 'https://speckle.xyz/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D' // Revit sample house (good for bim-like stuff with many display meshes) // 'https://speckle.xyz/streams/da9e320dad/commits/5388ef24b8' + 'https://app.speckle.systems/projects/da9e320dad/models/3f0dc4be35%405388ef24b8' // 'https://latest.speckle.dev/streams/58b5648c4d/commits/60371ecb2d' // 'Super' heavy revit shit // 'https://speckle.xyz/streams/e6f9156405/commits/0694d53bb5' @@ -141,7 +143,7 @@ const getStream = () => { // AutoCAD NEW // 'https://latest.speckle.dev/streams/3ed8357f29/commits/46905429f6' //Blizzard world - 'https://latest.speckle.dev/streams/0c6ad366c4/commits/aa1c393aec' + // 'https://latest.speckle.dev/streams/0c6ad366c4/commits/aa1c393aec' //Car // 'https://latest.speckle.dev/streams/17d2e25a97/commits/6b6cf3d43e' // Jonathon's diff --git a/packages/viewer/package.json b/packages/viewer/package.json index 7b23b7db14..b037623822 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -53,6 +53,7 @@ ], "dependencies": { "@speckle/objectloader": "workspace:^", + "@speckle/shared": "workspace:^", "@types/flat": "^5.0.2", "camera-controls": "^1.33.1", "flat": "^5.0.2", diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts index f41ec857c6..ca30e7228e 100644 --- a/packages/viewer/src/index.ts +++ b/packages/viewer/src/index.ts @@ -138,3 +138,5 @@ export type { MeasurementOptions, FilteringState } + +export * as UrlHelper from './modules/UrlHelper' diff --git a/packages/viewer/src/modules/UrlHelper.ts b/packages/viewer/src/modules/UrlHelper.ts new file mode 100644 index 0000000000..94539789dd --- /dev/null +++ b/packages/viewer/src/modules/UrlHelper.ts @@ -0,0 +1,310 @@ +import { SpeckleViewer } from '@speckle/shared' +import Logger from 'js-logger' + +interface ReferencedObjectUrl { + origin: string + projectId: string +} + +interface CommitReferencedObjectUrl { + origin: string + streamId: string + commitId: string +} + +export async function getResourceUrls( + url: string, + authToken?: string +): Promise { + /** I'm up for a better way of doing this */ + if (url.includes('streams')) return getOldResourceUrls(url, authToken) + return getNewResourceUrls(url, authToken) +} + +async function getOldResourceUrls(url: string, authToken?: string): Promise { + const parsed = new URL(url) + const streamId = url.split('/streams/')[1].substring(0, 10) + + const objsUrls = [] + // supports commit based urls + if (url.includes('commits')) { + const commitId = url.split('/commits/')[1].substring(0, 10) + const objUrl = await this.getCommitReferencedObjectUrl({ + origin: parsed.origin, + streamId, + commitId + }) + objsUrls.push(objUrl) + } + + // object based urls + if (url.includes('objects')) objsUrls.push(url) + + // supports urls that include overlay queries + // e.g., https://speckle.xyz/streams/a632e7a784/objects/457c45feffa6f954572e5e86fb6d4f25?overlay=cf8dc76247,f5adc1d991b3dceb4b5ad6b50f919a0e + if (url.includes('overlay=')) { + const searchParams = new URLSearchParams(parsed.search) + const resIds = searchParams.get('overlay')?.split(',') + if (resIds !== undefined) { + for (const resId of resIds) { + if (resId.length === 10) { + objsUrls.push( + await getCommitReferencedObjectUrl( + { + origin: parsed.origin, + streamId, + commitId: resId + } as CommitReferencedObjectUrl, + authToken + ) + ) + } else { + objsUrls.push(`${parsed.origin}/streams/${streamId}/objects/${resId}`) + } + } + } + } + + return objsUrls +} + +async function getCommitReferencedObjectUrl( + ref: CommitReferencedObjectUrl, + authToken?: string +) { + const headers: { 'Content-Type': string; Authorization: string } = { + 'Content-Type': 'application/json', + Authorization: '' + } + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}` + } + const res = await fetch(`${ref.origin}/graphql`, { + method: 'POST', + headers, + body: JSON.stringify({ + query: ` + query Stream($streamId: String!, $commitId: String!) { + stream(id: $streamId) { + commit(id: $commitId) { + referencedObject + } + } + } + `, + variables: { streamId: ref.streamId, commitId: ref.commitId } + }) + }) + + const { data } = await res.json() + return `${ref.origin}/streams/${ref.streamId}/objects/${data.stream.commit.referencedObject}` +} + +async function getNewResourceUrls(url: string, authToken?: string): Promise { + const parsed = new URL(decodeURI(url)) + const params = parsed.href.match(/[^/]+$/) + if (!params) { + return Promise.reject('No model or object ids specified') + } + + const projectId = parsed.href.split('/projects/')[1].substring(0, 10) + const headers: { 'Content-Type': string; Authorization: string } = { + 'Content-Type': 'application/json', + Authorization: '' + } + + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}` + } + + const ref: ReferencedObjectUrl = { + origin: parsed.origin, + projectId + } + + const resources = SpeckleViewer.ViewerRoute.parseUrlParameters( + decodeURIComponent(params[0]) + ) + + const promises = [] + for (let k = 0; k < resources.length; k++) { + const resource: SpeckleViewer.ViewerRoute.ViewerResource = resources[k] + + if (SpeckleViewer.ViewerRoute.isObjectResource(resource)) { + promises.push(objectResourceToUrl(ref, resource)) + } else if (SpeckleViewer.ViewerRoute.isModelResource(resource)) { + promises.push(modelResourceToUrl(headers, ref, resource)) + } else if (SpeckleViewer.ViewerRoute.isAllModelsResource(resource)) { + promises.push(modelAllResourceToUrl(headers, ref)) + } + } + + return (await Promise.all(promises)).flat() +} + +async function objectResourceToUrl( + ref: ReferencedObjectUrl, + resource: SpeckleViewer.ViewerRoute.ViewerObjectResource +): Promise { + return Promise.resolve( + `${ref.origin}/streams/${ref.projectId}/objects/${resource.toString()}` + ) +} + +async function modelResourceToUrl( + headers: { + 'Content-Type': string + Authorization: string + }, + ref: ReferencedObjectUrl, + resource: SpeckleViewer.ViewerRoute.ViewerModelResource +): Promise { + return resource.versionId + ? runModelVersionQuery(headers, ref, resource) + : runModelLastVersionQuery(headers, ref, resource) +} + +async function modelAllResourceToUrl( + headers: { + 'Content-Type': string + Authorization: string + }, + ref: ReferencedObjectUrl +): Promise { + return runAllModelsQuery(headers, ref) +} + +async function runModelLastVersionQuery( + headers: { 'Content-Type': string; Authorization: string }, + ref: ReferencedObjectUrl, + resource: SpeckleViewer.ViewerRoute.ViewerModelResource +): Promise { + const res = await fetch(`${ref.origin}/graphql`, { + method: 'POST', + headers, + body: JSON.stringify({ + query: ` + query ViewerUrlHelperModelLastVersion($modelId: String!, $projectId: String!) { + project(id: $projectId) { + model(id: $modelId) { + versions(limit: 1) { + items { + referencedObject + } + } + } + } + } + `, + variables: { + projectId: ref.projectId, + modelId: resource.modelId + } + }) + }) + try { + const data = await getResponse(res) + return `${ref.origin}/streams/${ref.projectId}/objects/${data.project.model.versions.items[0].referencedObject}` + } catch (e) { + Logger.error( + `Could not get object URLs for project ${ref.projectId} and model ${resource.modelId}. Error: ${e.message}` + ) + } + return '' +} + +async function runModelVersionQuery( + headers: { 'Content-Type': string; Authorization: string }, + ref: ReferencedObjectUrl, + resource: SpeckleViewer.ViewerRoute.ViewerModelResource +): Promise { + const res = await fetch(`${ref.origin}/graphql`, { + method: 'POST', + headers, + body: JSON.stringify({ + query: ` + query ViewerUrlHelperModelVersion($modelId: String!, $projectId: String!, $versionId: String!) { + project(id: $projectId) { + model(id: $modelId) { + version(id: $versionId) { + referencedObject + } + } + } + } + `, + variables: { + projectId: ref.projectId, + modelId: resource.modelId, + versionId: resource.versionId + } + }) + }) + try { + const data = await getResponse(res) + return `${ref.origin}/streams/${ref.projectId}/objects/${data.project.model.version.referencedObject}` + } catch (e) { + Logger.error( + `Could not get object URLs for project ${ref.projectId} and model ${resource.modelId}. Error: ${e.message}` + ) + } + return '' +} + +async function runAllModelsQuery( + headers: { 'Content-Type': string; Authorization: string }, + ref: ReferencedObjectUrl +): Promise { + const res = await fetch(`${ref.origin}/graphql`, { + method: 'POST', + headers, + body: JSON.stringify({ + query: ` + query ViewerUrlHelperAllModel($projectId: String!) { + project(id: $projectId) { + models { + items { + versions(limit: 1) { + items { + referencedObject + } + } + } + } + } + } + `, + variables: { + projectId: ref.projectId + } + }) + }) + try { + const data = await getResponse(res) + const urls: string[] = [] + data.project.models.items.forEach( + (element: { versions: { items: { referencedObject: string }[] } }) => { + urls.push( + `${ref.origin}/streams/${ref.projectId}/objects/${element.versions.items[0].referencedObject}` + ) + } + ) + return urls + } catch (e) { + Logger.error( + `Could not get object URLs for project ${ref.projectId}. Error: ${e.message}` + ) + } + return [''] +} + +async function getResponse(res: Response) { + const { data } = await res.json() + if (!data) throw new Error(`Query failed`) + + if (!data.project) throw new Error('Project not found') + + if (!data.project.model && !data.project.models) throw new Error('Model(s) not found') + + return data +} diff --git a/yarn.lock b/yarn.lock index ce0db00fd7..dfb27cd5d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14790,6 +14790,7 @@ __metadata: "@rollup/plugin-babel": ^5.3.1 "@rollup/plugin-image": ^3.0.2 "@speckle/objectloader": "workspace:^" + "@speckle/shared": "workspace:^" "@types/babel__core": ^7.20.1 "@types/flat": ^5.0.2 "@types/three": ^0.136.0