From 56058c04a37fef398122f014528dcfea0709e330 Mon Sep 17 00:00:00 2001 From: Alexandru Popovici Date: Wed, 26 Jul 2023 10:40:47 +0300 Subject: [PATCH 01/11] Alex/#1678 faster diff (#1688) * Sped up diffing by several orders of magnitude. Also started on a different more fancy approach to diffing involving boolean operations on object maps * Finished with boolean version of diffing. Improved the speed of both by 50% on top of the previous speed improvements * #1690 Completely transparent objects are ignored during picking via a toggle-able flag in renderer --- packages/viewer-sandbox/src/Sandbox.ts | 14 +- packages/viewer-sandbox/src/main.ts | 5 +- packages/viewer/package.json | 3 +- packages/viewer/src/modules/Differ.ts | 170 ++++++++++++++---- .../viewer/src/modules/SpeckleRenderer.ts | 20 ++- packages/viewer/src/modules/batching/Batch.ts | 1 + .../viewer/src/modules/batching/Batcher.ts | 13 +- .../viewer/src/modules/batching/LineBatch.ts | 6 + .../viewer/src/modules/batching/PointBatch.ts | 27 +++ .../viewer/src/modules/batching/TextBatch.ts | 4 +- yarn.lock | 8 + 11 files changed, 226 insertions(+), 45 deletions(-) diff --git a/packages/viewer-sandbox/src/Sandbox.ts b/packages/viewer-sandbox/src/Sandbox.ts index a23f1d93e..e34b982fe 100644 --- a/packages/viewer-sandbox/src/Sandbox.ts +++ b/packages/viewer-sandbox/src/Sandbox.ts @@ -982,16 +982,16 @@ export default class Sandbox { diffResult = await this.viewer.diff( //building // 'https://latest.speckle.dev/streams/aea12cab71/objects/bcf37136dea9fe9397cdfd84012f616a', - // 'https://latest.speckle.dev/streams/aea12cab71/objects/94af0a6b4eaa318647180f8c230cb867' + // 'https://latest.speckle.dev/streams/aea12cab71/objects/94af0a6b4eaa318647180f8c230cb867', // cubes // 'https://latest.speckle.dev/streams/aea12cab71/objects/d2510c59c203b73473f8bbfe637e0552', // 'https://latest.speckle.dev/streams/aea12cab71/objects/1c327da824fdb04629eb48675101d7b7', // sketchup // 'https://latest.speckle.dev/streams/aea12cab71/objects/06bed1819e6c61d9df7196d424ab1eec', - // 'https://latest.speckle.dev/streams/aea12cab71/objects/9026f1d6495789b9eab31b5028c9a8ef' + // 'https://latest.speckle.dev/streams/aea12cab71/objects/9026f1d6495789b9eab31b5028c9a8ef', //latest - 'https://latest.speckle.dev/streams/cdbe82b016/objects/c14d1a33fd68323193813ec215737472', - 'https://latest.speckle.dev/streams/cdbe82b016/objects/16676fc95a9ead877f6a825d9e28cbe8', + // 'https://latest.speckle.dev/streams/cdbe82b016/objects/c14d1a33fd68323193813ec215737472', + // 'https://latest.speckle.dev/streams/cdbe82b016/objects/16676fc95a9ead877f6a825d9e28cbe8', //lines // 'https://latest.speckle.dev/streams/92b620fb17/objects/3b42d6ef51d3110b4e33b9f8cdc9f357', // 'https://latest.speckle.dev/streams/92b620fb17/objects/774384d431fb34d447d4696abbc4b816', @@ -1016,6 +1016,12 @@ export default class Sandbox { // bug // 'https://latest.speckle.dev/streams/0c6ad366c4/objects/03f0a8bf0ed8064865eda87a865c7212', // 'https://latest.speckle.dev/streams/0c6ad366c4/objects/33ef6b9b547dc9688eb40157b967eab9', + // large + 'https://speckle.xyz/streams/e6f9156405/objects/650f358d8aac50168d9e9226ef6f5cbc', + 'https://latest.speckle.dev/streams/92b620fb17/objects/1154ca1d997ac631571db55f84cb703d', + // cubes + // 'https://latest.speckle.dev/streams/0c6ad366c4/objects/03f0a8bf0ed8064865eda87a865c7212', + // 'https://latest.speckle.dev/streams/0c6ad366c4/objects/33ef6b9b547dc9688eb40157b967eab9', VisualDiffMode.COLORED, localStorage.getItem('AuthTokenLatest') as string diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 393210a3b..778973f08 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -110,7 +110,7 @@ const getStream = () => { // prettier-ignore // '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://speckle.xyz/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.dev/streams/c1faab5c62/commits/6c6e43e5f3' // 'https://latest.speckle.dev/streams/58b5648c4d/commits/60371ecb2d' // 'Super' heavy revit shit @@ -278,11 +278,12 @@ const getStream = () => { // 'https://latest.speckle.dev/streams/b68abcbf2e/commits/4e94ecad62' // Big ass mafa' // 'https://speckle.xyz/streams/88307505eb/objects/a232d760059046b81ff97e6c4530c985' - // 'https://latest.speckle.dev/streams/92b620fb17/commits/dfb9ca025d' + 'https://latest.speckle.dev/streams/92b620fb17/commits/dfb9ca025d' // 'Blocks with elements // 'https://latest.speckle.dev/streams/e258b0e8db/commits/00e165cc1c' // 'https://latest.speckle.dev/streams/e258b0e8db/commits/e48cf53add' // 'https://latest.speckle.dev/streams/e258b0e8db/commits/c19577c7d6?c=%5B15.88776,-8.2182,12.17095,18.64059,1.48552,0.6025,0,1%5D' + // 'https://speckle.xyz/streams/46caea9b53/commits/71938adcd1' ) } diff --git a/packages/viewer/package.json b/packages/viewer/package.json index de27b0717..1b9a5e39e 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -61,7 +61,8 @@ "three": "^0.140.0", "three-mesh-bvh": "0.5.17", "tree-model": "1.0.7", - "troika-three-text": "0.47.2" + "troika-three-text": "0.47.2", + "underscore": "1.13.6" }, "devDependencies": { "@babel/core": "^7.18.2", diff --git a/packages/viewer/src/modules/Differ.ts b/packages/viewer/src/modules/Differ.ts index 208a6c99b..b64cb1777 100644 --- a/packages/viewer/src/modules/Differ.ts +++ b/packages/viewer/src/modules/Differ.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Color, FrontSide } from 'three' import { SpeckleTypeAllRenderables } from './converter/GeometryConverter' import SpeckleStandardMaterial from './materials/SpeckleStandardMaterial' @@ -7,6 +8,7 @@ import { GeometryType } from './batching/Batch' import SpeckleLineMaterial from './materials/SpeckleLineMaterial' import Logger from 'js-logger' import { NodeRenderView } from './tree/NodeRenderView' +import _, { omit } from 'underscore' // eslint-disable-next-line @typescript-eslint/no-explicit-any type SpeckleObject = Record @@ -184,10 +186,41 @@ export class Differ { this.removedMaterialPoint.toneMapped = false } - public diff(urlA: string, urlB: string): Promise { - const modifiedNew: Array = [] - const modifiedOld: Array = [] + private intersection(o1, o2) { + const [k1, k2] = [Object.keys(o1), Object.keys(o2)] + const [first, next] = k1.length > k2.length ? [k2, o1] : [k1, o2] + return first.filter((k) => k in next) + } + private buildIdMaps( + rvs: Array, + idMap: { [id: string]: { node: TreeNode; applicationId: string } }, + appIdMap: { [id: string]: number } + ) { + for (let k = 0; k < rvs.length; k++) { + const atomicRv = rvs[k] + const applicationId = atomicRv.model.raw.applicationId + ? atomicRv.model.raw.applicationId + : this.tree + .getAncestors(atomicRv) + .find((value) => value.model.raw.applicationId)?.model.raw.applicationId + + idMap[atomicRv.model.raw.id] = { + node: atomicRv, + applicationId + } + if (applicationId) { + appIdMap[applicationId] = 1 + } + } + } + + public diff(urlA: string, urlB: string): Promise { + return this.diffIterative(urlA, urlB) + } + + private diffBoolean(urlA: string, urlB: string): Promise { + const start = performance.now() const diffResult: DiffResult = { unchanged: [], added: [], @@ -197,8 +230,6 @@ export class Differ { const renderTreeA = this.tree.getRenderTree(urlA) const renderTreeB = this.tree.getRenderTree(urlB) - const rootA = this.tree.findId(urlA) - const rootB = this.tree.findId(urlB) let rvsA = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables) let rvsB = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables) @@ -213,18 +244,106 @@ export class Differ { rvsA = [...Array.from(new Set(rvsA))] rvsB = [...Array.from(new Set(rvsB))] + const idMapA = {} + const appIdMapA = {} + this.buildIdMaps(rvsA, idMapA, appIdMapA) + + const idMapB = {} + const appIdMapB = {} + this.buildIdMaps(rvsB, idMapB, appIdMapB) + + /** Get the ids which are common between the two maps. This will be objects + * which have not changed + */ + const unchanged: Array = this.intersection(idMapA, idMapB) + /** We remove the unchanged objects from B and end up with changed + added */ + const addedModified = _.omit(idMapB, unchanged) + /** We remove the unchanged objects from A and end up with changed + removed */ + const removedModified = _.omit(idMapA, unchanged) + /** We remove the changed objects from B. An object from B is changed if + * it's application ID exists in A + */ + const added = _.omit(addedModified, function (value, key, object) { + return value.applicationId && appIdMapA[value.applicationId] !== undefined + }) + /** We remove the changed objects from A. An object from A is changed if + * it's application ID exists in B + */ + const removed = _.omit(removedModified, function (value, key, object) { + return value.applicationId && appIdMapB[value.applicationId] !== undefined + }) + /** We remove the removed objects from A, leaving us only changed objects */ + const modifiedRemoved = _.omit(removedModified, Object.keys(removed)) + /** We remove the removed objects from B, leaving us only changed objects */ + const modifiedAdded = _.omit(addedModified, Object.keys(added)) + + /** We fill the arrays from here on out */ + const modifiedOld = Object.values(modifiedRemoved).map( + (value: { node: TreeNode }) => value.node + ) + const modifiedNew = Object.values(modifiedAdded).map( + (value: { node: TreeNode }) => value.node + ) + diffResult.unchanged.push(...unchanged.map((value) => idMapA[value].node)) + diffResult.unchanged.push(...unchanged.map((value) => idMapB[value].node)) + diffResult.removed.push( + ...Object.values(removed).map((value: { node: TreeNode }) => value.node) + ) + diffResult.added.push( + ...Object.values(added).map((value: { node: TreeNode }) => value.node) + ) + + modifiedOld.forEach((value, index) => { + value + diffResult.modified.push([modifiedOld[index], modifiedNew[index]]) + }) + console.warn('Boolean Time -> ', performance.now() - start) + return Promise.resolve(diffResult) + } + + private diffIterative(urlA: string, urlB: string): Promise { + const start = performance.now() + const modifiedNew: Array = [] + const modifiedOld: Array = [] + + const diffResult: DiffResult = { + unchanged: [], + added: [], + removed: [], + modified: [] + } + + const renderTreeA = this.tree.getRenderTree(urlA) + const renderTreeB = this.tree.getRenderTree(urlB) + let rvsA = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables) + let rvsB = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables) + + rvsA = rvsA.map((value) => { + return renderTreeA.getAtomicParent(value) + }) + + rvsB = rvsB.map((value) => { + return renderTreeB.getAtomicParent(value) + }) + + rvsA = [...Array.from(new Set(rvsA))] + rvsB = [...Array.from(new Set(rvsB))] + + const idMapA = {} + const appIdMapA = {} + this.buildIdMaps(rvsA, idMapA, appIdMapA) + + const idMapB = {} + const appIdMapB = {} + this.buildIdMaps(rvsB, idMapB, appIdMapB) + for (let k = 0; k < rvsB.length; k++) { - const res = rootA.first((node: TreeNode) => { - return rvsB[k].model.raw.id === node.model.raw.id - }) + const res = idMapA[rvsB[k].model.raw.id]?.node + if (res) { diffResult.unchanged.push(res) } else { - const applicationId = rvsB[k].model.raw.applicationId - ? rvsB[k].model.raw.applicationId - : this.tree - .getAncestors(rvsB[k]) - .find((value) => value.model.raw.applicationId) + const applicationId = idMapB[rvsB[k].model.raw.id].applicationId if (!applicationId) { Logger.error( `No application ID found. Object id:${rvsB[k].model.raw.id} is considered 'added'!` @@ -232,9 +351,7 @@ export class Differ { diffResult.added.push(rvsB[k]) continue } - const res2 = rootA.first((node: TreeNode) => { - return applicationId === node.model.raw.applicationId - }) + const res2 = appIdMapA[applicationId] if (res2) { modifiedNew.push(rvsB[k]) } else { @@ -242,17 +359,10 @@ export class Differ { } } } - for (let k = 0; k < rvsA.length; k++) { - const res = rootB.first((node: TreeNode) => { - return rvsA[k].model.raw.id === node.model.raw.id - }) + const res = idMapB[rvsA[k].model.raw.id]?.node if (!res) { - const applicationId = rvsA[k].model.raw.applicationId - ? rvsA[k].model.raw.applicationId - : this.tree - .getAncestors(rvsA[k]) - .find((value) => value.model.raw.applicationId) + const applicationId = idMapA[rvsA[k].model.raw.id].applicationId if (!applicationId) { Logger.error( `No application ID found. Object id:${rvsA[k].model.raw.id} is considered 'removed'!` @@ -260,9 +370,7 @@ export class Differ { diffResult.removed.push(rvsA[k]) continue } - const res2 = rootB.first((node: TreeNode) => { - return applicationId === node.model.raw.applicationId - }) + const res2 = appIdMapB[applicationId] if (!res2) { diffResult.removed.push(rvsA[k]) } else { @@ -272,13 +380,11 @@ export class Differ { diffResult.unchanged.push(res) } } - modifiedOld.forEach((value, index) => { value diffResult.modified.push([modifiedOld[index], modifiedNew[index]]) }) - - console.warn(diffResult) + console.warn('Interative Time -> ', performance.now() - start) return Promise.resolve(diffResult) } @@ -322,6 +428,7 @@ export class Differ { [id: string]: SpeckleStandardMaterial | SpecklePointMaterial | SpeckleLineMaterial } ) { + const start = performance.now() switch (mode) { case VisualDiffMode.COLORED: this._materialGroups = this.getColoredMaterialGroups( @@ -337,6 +444,7 @@ export class Differ { default: Logger.error(`Unsupported visual diff mode ${mode}`) } + console.warn('Material groups -> ', performance.now() - start) return this._materialGroups } diff --git a/packages/viewer/src/modules/SpeckleRenderer.ts b/packages/viewer/src/modules/SpeckleRenderer.ts index 3b259f874..566612247 100644 --- a/packages/viewer/src/modules/SpeckleRenderer.ts +++ b/packages/viewer/src/modules/SpeckleRenderer.ts @@ -77,6 +77,7 @@ export enum ObjectLayers { export default class SpeckleRenderer { private readonly SHOW_HELPERS = false + private readonly IGNORE_ZERO_OPACITY_OBJECTS = true public SHOW_BVH = false private container: HTMLElement private _renderer: WebGLRenderer @@ -890,13 +891,28 @@ export default class SpeckleRenderer { const rvs = [] const points = [] for (let k = 0; k < results.length; k++) { - let rv = results[k].batchObject?.renderView - if (!rv) { + const batchObject = results[k].batchObject + let rv = null + if (batchObject) { + rv = batchObject.renderView + const material = (results[k].object as SpeckleMesh).getBatchObjectMaterial( + results[k].batchObject + ) + if (material.opacity === 0 && this.IGNORE_ZERO_OPACITY_OBJECTS) continue + } else { rv = this.batcher.getRenderView( results[k].object.uuid, results[k].faceIndex !== undefined ? results[k].faceIndex : results[k].index ) + if (rv) { + const material = this.batcher.getRenderViewMaterial( + results[k].object.uuid, + results[k].faceIndex !== undefined ? results[k].faceIndex : results[k].index + ) + if (material.opacity === 0 && this.IGNORE_ZERO_OPACITY_OBJECTS) continue + } } + if (rv) { rvs.push(rv) points.push(results[k].point) diff --git a/packages/viewer/src/modules/batching/Batch.ts b/packages/viewer/src/modules/batching/Batch.ts index 16cd863a1..9ea306301 100644 --- a/packages/viewer/src/modules/batching/Batch.ts +++ b/packages/viewer/src/modules/batching/Batch.ts @@ -31,6 +31,7 @@ export interface Batch { resetDrawRanges() buildBatch() getRenderView(index: number): NodeRenderView + getMaterialAtIndex(index: number): Material onUpdate(deltaTime: number) onRender(renderer: WebGLRenderer) purge() diff --git a/packages/viewer/src/modules/batching/Batcher.ts b/packages/viewer/src/modules/batching/Batcher.ts index dd10dae51..b360a2ffb 100644 --- a/packages/viewer/src/modules/batching/Batcher.ts +++ b/packages/viewer/src/modules/batching/Batcher.ts @@ -460,6 +460,15 @@ export default class Batcher { return this.batches[batchId].getRenderView(index) } + public getRenderViewMaterial(batchId: string, index: number) { + if (!this.batches[batchId]) { + Logger.error('Invalid batch id!') + return null + } + + return this.batches[batchId].getMaterialAtIndex(index) + } + public resetBatchesDrawRanges() { for (const k in this.batches) { this.batches[k].resetDrawRanges() @@ -576,7 +585,7 @@ export default class Batcher { if (k !== rv.batchId) { this.batches[k].setDrawRanges({ offset: 0, - count: Infinity, + count: this.batches[k].getCount(), material: this.materials.getFilterMaterial( this.batches[k].renderViews[0], FilterMaterialType.GHOST @@ -591,7 +600,7 @@ export default class Batcher { if (k !== batchId) { this.batches[k].setDrawRanges({ offset: 0, - count: Infinity, + count: this.batches[k].getCount(), material: this.materials.getFilterMaterial( this.batches[k].renderViews[0], FilterMaterialType.GHOST diff --git a/packages/viewer/src/modules/batching/LineBatch.ts b/packages/viewer/src/modules/batching/LineBatch.ts index 5e64ea5c2..23085fa79 100644 --- a/packages/viewer/src/modules/batching/LineBatch.ts +++ b/packages/viewer/src/modules/batching/LineBatch.ts @@ -5,6 +5,7 @@ import { InstancedInterleavedBuffer, InterleavedBufferAttribute, Line, + Material, Object3D, Vector4, WebGLRenderer @@ -253,6 +254,11 @@ export default class LineBatch implements Batch { } } + public getMaterialAtIndex(index: number): Material { + index + return this.batchMaterial + } + private makeLineGeometry(position: Float64Array) { this.geometry = this.makeLineGeometryTriangle(new Float32Array(position)) Geometry.updateRTEGeometry(this.geometry, position) diff --git a/packages/viewer/src/modules/batching/PointBatch.ts b/packages/viewer/src/modules/batching/PointBatch.ts index 879ab2ac5..c05143600 100644 --- a/packages/viewer/src/modules/batching/PointBatch.ts +++ b/packages/viewer/src/modules/batching/PointBatch.ts @@ -17,6 +17,7 @@ import { } from './Batch' import { GeometryConverter } from '../converter/GeometryConverter' import { ObjectLayers } from '../SpeckleRenderer' +import Logger from 'js-logger' export default class PointBatch implements Batch { public id: string @@ -353,6 +354,32 @@ export default class PointBatch implements Batch { } } + public getMaterialAtIndex(index: number): Material { + for (let k = 0; k < this.renderViews.length; k++) { + if ( + index >= this.renderViews[k].batchStart && + index < this.renderViews[k].batchEnd + ) { + const rv = this.renderViews[k] + const group = this.geometry.groups.find((value) => { + return ( + rv.batchStart >= value.start && + rv.batchStart + rv.batchCount <= value.count + value.start + ) + }) + if (!Array.isArray(this.mesh.material)) { + return this.mesh.material + } else { + if (!group) { + Logger.warn(`Malformed material index!`) + return null + } + return this.mesh.material[group.materialIndex] + } + } + } + } + private makePointGeometry( position: Float64Array, color: Float32Array diff --git a/packages/viewer/src/modules/batching/TextBatch.ts b/packages/viewer/src/modules/batching/TextBatch.ts index 63c127c20..a14721b11 100644 --- a/packages/viewer/src/modules/batching/TextBatch.ts +++ b/packages/viewer/src/modules/batching/TextBatch.ts @@ -131,9 +131,7 @@ export default class TextBatch implements Batch { } public getMaterialAtIndex(index: number): Material { - index - console.warn('Deprecated! Do not call this anymore') - return null + return this.batchMaterial } public purge() { diff --git a/yarn.lock b/yarn.lock index 6244778b7..7dab210a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12881,6 +12881,7 @@ __metadata: tree-model: 1.0.7 troika-three-text: 0.47.2 typescript: ^4.5.4 + underscore: 1.13.6 languageName: unknown linkType: soft @@ -43510,6 +43511,13 @@ __metadata: languageName: node linkType: hard +"underscore@npm:1.13.6": + version: 1.13.6 + resolution: "underscore@npm:1.13.6" + checksum: d5cedd14a9d0d91dd38c1ce6169e4455bb931f0aaf354108e47bd46d3f2da7464d49b2171a5cf786d61963204a42d01ea1332a903b7342ad428deaafaf70ec36 + languageName: node + linkType: hard + "undici@npm:^5.1.0, undici@npm:^5.12.0, undici@npm:^5.19.1, undici@npm:^5.22.0, undici@npm:^5.8.0": version: 5.22.1 resolution: "undici@npm:5.22.1" From ea941b3b8197997d876d1bacecfa9536aa253bfe Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Wed, 26 Jul 2023 12:38:50 +0100 Subject: [PATCH 02/11] chore(deps): bump node to 18.17.0 (#1722) --- .circleci/config.yml | 12 ++++++------ package.json | 2 +- packages/fileimport-service/Dockerfile | 4 ++-- packages/fileimport-service/package.json | 2 +- packages/frontend/Dockerfile | 2 +- packages/frontend/package.json | 2 +- packages/objectloader/package.json | 2 +- packages/preview-service/Dockerfile | 4 ++-- packages/preview-service/package.json | 2 +- packages/server/Dockerfile | 6 +++--- packages/server/package.json | 2 +- packages/shared/package.json | 2 +- packages/viewer/package.json | 2 +- packages/webhook-service/Dockerfile | 4 ++-- packages/webhook-service/package.json | 2 +- 15 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5f99e630f..fdedadb86 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -322,7 +322,7 @@ jobs: test-server: docker: - - image: cimg/node:18.16.1 + - image: cimg/node:18.17.0 - image: cimg/redis:7.0.7 - image: 'cimg/postgres:14.5' environment: @@ -417,7 +417,7 @@ jobs: test-frontend-2: docker: - - image: cimg/node:18.16.1-browsers + - image: cimg/node:18.17.0-browsers resource_class: xlarge steps: - checkout @@ -460,7 +460,7 @@ jobs: test-dui-3: docker: - - image: cimg/node:18.16.1 + - image: cimg/node:18.17.0 resource_class: medium+ steps: - checkout @@ -490,7 +490,7 @@ jobs: test-ui-components: docker: - - image: cimg/node:18.16.1-browsers + - image: cimg/node:18.17.0-browsers resource_class: xlarge steps: - checkout @@ -544,7 +544,7 @@ jobs: frontend-2-chromatic: resource_class: medium+ docker: - - image: cimg/node:18.16.1 + - image: cimg/node:18.17.0 steps: - checkout - restore_cache: @@ -578,7 +578,7 @@ jobs: ui-components-chromatic: resource_class: medium+ docker: - - image: cimg/node:18.16.1 + - image: cimg/node:18.17.0 steps: - checkout - restore_cache: diff --git a/package.json b/package.json index b753d571c..f37d27b85 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "name": "root", "private": true, "engines": { - "node": "^18.16.1" + "node": "^18.17.0" }, "scripts": { "build": "yarn workspaces foreach -ptv run build", diff --git a/packages/fileimport-service/Dockerfile b/packages/fileimport-service/Dockerfile index 980debeb9..78013074f 100644 --- a/packages/fileimport-service/Dockerfile +++ b/packages/fileimport-service/Dockerfile @@ -1,6 +1,6 @@ ARG NODE_ENV=production -FROM node:18.16.1-bullseye-slim as build-stage +FROM node:18.17.0-bullseye-slim as build-stage ARG NODE_ENV ENV NODE_ENV=${NODE_ENV} @@ -47,7 +47,7 @@ RUN apt-get update && \ COPY packages/fileimport-service/requirements.txt /speckle-server/ RUN /venv/bin/pip install --disable-pip-version-check --no-cache-dir --requirement /speckle-server/requirements.txt -FROM node:18.16.1-bullseye-slim as dependency-stage +FROM node:18.17.0-bullseye-slim as dependency-stage # installing just the production dependencies # separate stage to avoid including development dependencies ARG NODE_ENV diff --git a/packages/fileimport-service/package.json b/packages/fileimport-service/package.json index 950803587..a7f90244b 100644 --- a/packages/fileimport-service/package.json +++ b/packages/fileimport-service/package.json @@ -12,7 +12,7 @@ "url": "git+https://github.com/specklesystems/speckle-server.git" }, "engines": { - "node": "^18.16.1" + "node": "^18.17.0" }, "scripts": { "dev": "cross-env POSTGRES_URL=postgres://speckle:speckle@localhost/speckle NODE_ENV=development LOG_PRETTY=true SPECKLE_SERVER_URL=http://localhost:3000 nodemon --no-experimental-fetch ./src/daemon.js", diff --git a/packages/frontend/Dockerfile b/packages/frontend/Dockerfile index 3b143e5fe..4c67aef9f 100644 --- a/packages/frontend/Dockerfile +++ b/packages/frontend/Dockerfile @@ -2,7 +2,7 @@ ARG NODE_ENV=production ARG SPECKLE_SERVER_VERSION=custom # build stage -FROM node:18.16.1-bullseye-slim as build-stage +FROM node:18.17.0-bullseye-slim as build-stage ARG NODE_ENV ARG SPECKLE_SERVER_VERSION diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 179f5b35b..3b0b7749f 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -97,6 +97,6 @@ "vue-tsc": "^1.0.8" }, "engines": { - "node": "^18.16.1" + "node": "^18.17.0" } } diff --git a/packages/objectloader/package.json b/packages/objectloader/package.json index 2da0bd08c..1b1aaf0de 100644 --- a/packages/objectloader/package.json +++ b/packages/objectloader/package.json @@ -12,7 +12,7 @@ "directory": "packages/objectloader" }, "engines": { - "node": "^18.16.1" + "node": "^18.17.0" }, "scripts": { "lint": "eslint . --ext .js,.ts", diff --git a/packages/preview-service/Dockerfile b/packages/preview-service/Dockerfile index f2ca8282e..2d5c60fd9 100644 --- a/packages/preview-service/Dockerfile +++ b/packages/preview-service/Dockerfile @@ -1,7 +1,7 @@ # NOTE: Docker context should be set to git root directory, to include the viewer ARG NODE_ENV=production -FROM node:18.16.1-bullseye-slim as build-stage +FROM node:18.17.0-bullseye-slim as build-stage ARG NODE_ENV ENV NODE_ENV=${NODE_ENV} @@ -36,7 +36,7 @@ COPY packages/preview-service ./packages/preview-service/ # This way the foreach only builds the frontend and its deps RUN yarn workspaces foreach run build -FROM node:18.16.1-bullseye-slim as node +FROM node:18.17.0-bullseye-slim as node RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y \ diff --git a/packages/preview-service/package.json b/packages/preview-service/package.json index de7637a3b..7902d6739 100644 --- a/packages/preview-service/package.json +++ b/packages/preview-service/package.json @@ -11,7 +11,7 @@ "directory": "packages/preview-service" }, "engines": { - "node": "^18.16.1" + "node": "^18.17.0" }, "scripts": { "dev": "LOG_PRETTY=true nodemon --trace-deprecation ./bin/www", diff --git a/packages/server/Dockerfile b/packages/server/Dockerfile index a91770d9c..bb54f88f9 100644 --- a/packages/server/Dockerfile +++ b/packages/server/Dockerfile @@ -1,7 +1,7 @@ ARG NODE_ENV=production ARG SPECKLE_SERVER_VERSION=custom -FROM node:18.16.1-bullseye-slim as build-stage +FROM node:18.17.0-bullseye-slim as build-stage ARG NODE_ENV ARG SPECKLE_SERVER_VERSION WORKDIR /speckle-server @@ -39,7 +39,7 @@ RUN yarn workspaces foreach run build # install only production dependencies # we need a clean environment, free of build dependencies -FROM node:18.16.1-bullseye-slim as dependency-stage +FROM node:18.17.0-bullseye-slim as dependency-stage ARG NODE_ENV ARG SPECKLE_SERVER_VERSION @@ -56,7 +56,7 @@ COPY packages/objectloader/package.json ./packages/objectloader/ WORKDIR /speckle-server/packages/server RUN yarn workspaces focus --production -FROM node:18.16.1-bullseye-slim as production-stage +FROM node:18.17.0-bullseye-slim as production-stage ARG NODE_ENV ARG SPECKLE_SERVER_VERSION ARG FILE_SIZE_LIMIT_MB=100 diff --git a/packages/server/package.json b/packages/server/package.json index f05fbfb10..ec7cb0ad3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/specklesystems/Server.git" }, "engines": { - "node": "^18.16.1" + "node": "^18.17.0" }, "scripts": { "build": "tsc -p ./tsconfig.build.json", diff --git a/packages/shared/package.json b/packages/shared/package.json index 2dd686fb8..47c325706 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -22,7 +22,7 @@ }, "sideEffects": false, "engines": { - "node": "^18.16.1" + "node": "^18.17.0" }, "author": "AEC Systems", "license": "Apache-2.0", diff --git a/packages/viewer/package.json b/packages/viewer/package.json index 1b9a5e39e..1500ae97f 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -30,7 +30,7 @@ "dist" ], "engines": { - "node": "^18.16.1" + "node": "^18.17.0" }, "scripts": { "build": "NODE_ENV=production rollup --config", diff --git a/packages/webhook-service/Dockerfile b/packages/webhook-service/Dockerfile index c49b2e48c..40dea937a 100644 --- a/packages/webhook-service/Dockerfile +++ b/packages/webhook-service/Dockerfile @@ -1,6 +1,6 @@ ARG NODE_ENV=production -FROM node:18.16.1-bullseye-slim as build-stage +FROM node:18.17.0-bullseye-slim as build-stage ARG NODE_ENV ENV NODE_ENV=${NODE_ENV} @@ -32,7 +32,7 @@ ENV TINI_VERSION=${TINI_VERSION} ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini ./tini RUN chmod +x ./tini -FROM node:18.16.1-bullseye-slim as dependency-stage +FROM node:18.17.0-bullseye-slim as dependency-stage # yarn install ARG NODE_ENV ENV NODE_ENV=${NODE_ENV} diff --git a/packages/webhook-service/package.json b/packages/webhook-service/package.json index 32c04d86c..90f49c957 100644 --- a/packages/webhook-service/package.json +++ b/packages/webhook-service/package.json @@ -13,7 +13,7 @@ }, "homepage": "https://github.com/specklesystems/speckle-server#readme", "engines": { - "node": "^18.16.1" + "node": "^18.17.0" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", From 783e785b32f01d9e4e8113d6d58b1a77e2ff2f90 Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Fri, 28 Jul 2023 15:17:30 +0100 Subject: [PATCH 03/11] fix(/api/diff): return 400 if greater than max objects (#1736) - log a warning - return a 400 - this is a workaround for a limitation of unsigned int-16 in database --- packages/server/modules/core/rest/diffUpload.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/server/modules/core/rest/diffUpload.js b/packages/server/modules/core/rest/diffUpload.js index 76161b76a..90b573b17 100644 --- a/packages/server/modules/core/rest/diffUpload.js +++ b/packages/server/modules/core/rest/diffUpload.js @@ -6,6 +6,8 @@ const { validatePermissionsWriteStream } = require('./authUtils') const { hasObjects } = require('../services/objects') +const MAXIMUM_OBJECTS = 65536 + module.exports = (app) => { app.options('/api/diff/:streamId', corsMiddleware()) @@ -23,6 +25,12 @@ module.exports = (app) => { } const objectList = JSON.parse(req.body.objects) + if (objectList.length > MAXIMUM_OBJECTS) { + req.log.warn( + `User ${req.context.userId} tried to diff ${objectList.length} objects, which is greater than the maximum of ${MAXIMUM_OBJECTS}.` + ) + return res.status(400).end(`Too many objects. Maximum ${MAXIMUM_OBJECTS}.`) + } req.log.info(`Diffing ${objectList.length} objects.`) From a7ca2f482c91692e31057184d78402841b253ab2 Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Sat, 29 Jul 2023 18:30:56 +0100 Subject: [PATCH 04/11] fix(/api/diff): chunk large lists of objects (#1737) * Revert "fix(/api/diff): return 400 if greater than max objects (#1736)" This reverts commit 783e785b32f01d9e4e8113d6d58b1a77e2ff2f90. * fix(/api/diff): chunk long object lists to remain within maximum length --- .../server/modules/core/rest/diffUpload.js | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/server/modules/core/rest/diffUpload.js b/packages/server/modules/core/rest/diffUpload.js index 90b573b17..ef40dfe37 100644 --- a/packages/server/modules/core/rest/diffUpload.js +++ b/packages/server/modules/core/rest/diffUpload.js @@ -6,7 +6,7 @@ const { validatePermissionsWriteStream } = require('./authUtils') const { hasObjects } = require('../services/objects') -const MAXIMUM_OBJECTS = 65536 +const { chunk } = require('lodash') module.exports = (app) => { app.options('/api/diff/:streamId', corsMiddleware()) @@ -25,19 +25,22 @@ module.exports = (app) => { } const objectList = JSON.parse(req.body.objects) - if (objectList.length > MAXIMUM_OBJECTS) { - req.log.warn( - `User ${req.context.userId} tried to diff ${objectList.length} objects, which is greater than the maximum of ${MAXIMUM_OBJECTS}.` - ) - return res.status(400).end(`Too many objects. Maximum ${MAXIMUM_OBJECTS}.`) - } req.log.info(`Diffing ${objectList.length} objects.`) - const response = await hasObjects({ - streamId: req.params.streamId, - objectIds: objectList - }) + const chunkSize = 1000 + const objectListChunks = chunk(objectList, chunkSize) + const mappedObjects = await Promise.all( + objectListChunks.map((objectListChunk) => + hasObjects({ + streamId: req.params.streamId, + objectIds: objectListChunk + }) + ) + ) + const response = {} + Object.assign(response, ...mappedObjects) + req.log.debug(response) res.writeHead(200, { 'Content-Encoding': 'gzip', From 951d86aa3e92aa35e63a1b179b8989a982a2471f Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Mon, 31 Jul 2023 11:44:39 +0300 Subject: [PATCH 05/11] feat(server): cross-server onboarding stream sync + DL project CLI command (#1717) * WIP * cleanup and sync MVP kinda done * WIP * logging improved a bit * fixed version sorting * onboarding base stream creation works * moved onboarding stream to use new base --- packages/server/.env-example | 8 +- packages/server/codegen.yml | 10 + packages/server/logging/logging.ts | 3 + .../modules/cli/commands/download/commit.ts | 5 +- .../modules/cli/commands/download/project.ts | 36 + .../comments/services/commentTextService.ts | 8 +- packages/server/modules/core/dbSchema.ts | 8 +- .../modules/core/repositories/streams.ts | 12 +- .../server/modules/core/repositories/users.ts | 10 + .../core/services/streams/onboarding.ts | 36 +- .../modules/cross-server-sync/errors/index.ts | 11 + .../graph/generated/graphql.ts | 2689 +++++++++++++++++ .../server/modules/cross-server-sync/index.ts | 17 + .../services}/commit.ts | 307 +- .../services/onboardingProject.ts | 70 + .../cross-server-sync/services/project.ts | 227 ++ .../cross-server-sync/utils/graphqlClient.ts | 79 + packages/server/modules/index.js | 5 +- .../modules/shared/helpers/envHelper.ts | 33 +- .../modules/shared/helpers/typeHelper.ts | 2 +- 20 files changed, 3373 insertions(+), 203 deletions(-) create mode 100644 packages/server/modules/cli/commands/download/project.ts create mode 100644 packages/server/modules/cross-server-sync/errors/index.ts create mode 100644 packages/server/modules/cross-server-sync/graph/generated/graphql.ts create mode 100644 packages/server/modules/cross-server-sync/index.ts rename packages/server/modules/{cli/services/download => cross-server-sync/services}/commit.ts (69%) create mode 100644 packages/server/modules/cross-server-sync/services/onboardingProject.ts create mode 100644 packages/server/modules/cross-server-sync/services/project.ts create mode 100644 packages/server/modules/cross-server-sync/utils/graphqlClient.ts diff --git a/packages/server/.env-example b/packages/server/.env-example index ef5713dec..2ec1188b3 100644 --- a/packages/server/.env-example +++ b/packages/server/.env-example @@ -22,9 +22,11 @@ REDIS_URL="redis://localhost:6379" USE_FRONTEND_2=false FRONTEND_ORIGIN="http://localhost:8081" -# Stream to be used as the demo/tutorial stream in onboarding flows -# (if not set/valid, stream will be a blank one) -ONBOARDING_STREAM_ID= +# URL of a project on any FE2 speckle server that will be pulled in and used as the onboarding stream +ONBOARDING_STREAM_URL=https://latest.speckle.systems/projects/843d07eb10 + +# Increase this value to re-sync the onboarding stream +ONBOARDING_STREAM_CACHE_BUST_NUMBER=1 ############################################################ # Postgres Database diff --git a/packages/server/codegen.yml b/packages/server/codegen.yml index 00f1e3aa5..48554bd8c 100644 --- a/packages/server/codegen.yml +++ b/packages/server/codegen.yml @@ -28,6 +28,16 @@ generates: Comment: '@/modules/comments/helpers/graphTypes#CommentGraphQLReturn' PendingStreamCollaborator: '@/modules/serverinvites/helpers/graphTypes#PendingStreamCollaboratorGraphQLReturn' FileUpload: '@/modules/fileuploads/helpers/types#FileUploadGraphQLReturn' + modules/cross-server-sync/graph/generated/graphql.ts: + plugins: + - 'typescript' + - 'typescript-operations' + documents: + - 'modules/cross-server-sync/**/*.{js,ts}' + config: + scalars: + JSONObject: Record + DateTime: string test/graphql/generated/graphql.ts: plugins: - 'typescript' diff --git a/packages/server/logging/logging.ts b/packages/server/logging/logging.ts index dd2254855..da46f58f4 100644 --- a/packages/server/logging/logging.ts +++ b/packages/server/logging/logging.ts @@ -28,4 +28,7 @@ export const dbNotificationLogger = extendLoggerComponent(logger, 'db-notificati export const mixpanelLogger = extendLoggerComponent(logger, 'mixpanel') export const graphqlLogger = extendLoggerComponent(logger, 'graphql') export const authLogger = extendLoggerComponent(logger, 'auth') +export const crossServerSyncLogger = extendLoggerComponent(logger, 'cross-server-sync') + +export type Logger = typeof logger export { extendLoggerComponent } diff --git a/packages/server/modules/cli/commands/download/commit.ts b/packages/server/modules/cli/commands/download/commit.ts index 5c0f71d09..179461baa 100644 --- a/packages/server/modules/cli/commands/download/commit.ts +++ b/packages/server/modules/cli/commands/download/commit.ts @@ -1,5 +1,6 @@ import { CommandModule } from 'yargs' -import { downloadCommit } from '@/modules/cli/services/download/commit' +import { downloadCommit } from '@/modules/cross-server-sync/services/commit' +import { cliLogger } from '@/logging/logging' const command: CommandModule< unknown, @@ -41,7 +42,7 @@ const command: CommandModule< } }, handler: async (argv) => { - await downloadCommit(argv) + await downloadCommit(argv, { logger: cliLogger }) } } diff --git a/packages/server/modules/cli/commands/download/project.ts b/packages/server/modules/cli/commands/download/project.ts new file mode 100644 index 000000000..7f0daf5ec --- /dev/null +++ b/packages/server/modules/cli/commands/download/project.ts @@ -0,0 +1,36 @@ +import { CommandModule } from 'yargs' +import { cliLogger } from '@/logging/logging' +import { downloadProject } from '@/modules/cross-server-sync/services/project' + +const command: CommandModule< + unknown, + { + projectUrl: string + authorId: string + syncComments: boolean + } +> = { + command: 'project [syncComments]', + describe: 'Download a project from an external Speckle server instance', + builder: { + projectUrl: { + describe: + 'Public Project URL (e.g. https://latest.speckle.systems/projects/594d657cdd)', + type: 'string' + }, + authorId: { + describe: 'ID of the local user that will own the project', + type: 'string' + }, + syncComments: { + describe: 'Whether or not to sync comments as well', + type: 'boolean', + default: true + } + }, + handler: async (argv) => { + await downloadProject(argv, { logger: cliLogger }) + } +} + +export = command diff --git a/packages/server/modules/comments/services/commentTextService.ts b/packages/server/modules/comments/services/commentTextService.ts index 9b305b2ee..358d159ec 100644 --- a/packages/server/modules/comments/services/commentTextService.ts +++ b/packages/server/modules/comments/services/commentTextService.ts @@ -37,7 +37,13 @@ export function buildCommentTextFromInput({ }>) { if ((!isTextEditorDoc(doc) || isDocEmpty(doc)) && !blobIds.length) { throw new RichTextParseError( - 'Attempting to build comment text without document & attachments!' + 'Attempting to build comment text without document & attachments!', + { + info: { + doc, + blobIds + } + } ) } diff --git a/packages/server/modules/core/dbSchema.ts b/packages/server/modules/core/dbSchema.ts index 227041382..7ba1d6bc0 100644 --- a/packages/server/modules/core/dbSchema.ts +++ b/packages/server/modules/core/dbSchema.ts @@ -66,7 +66,7 @@ type MetaInnerSchemaConfig< * Get meta keys individually */ metaKey: { - [keyName in MK]: string + [keyName in MK]: keyName } /** @@ -186,8 +186,8 @@ function buildMetaTableHelper - ), + {} as Record + ) as { [keyName in MK]: keyName }, parentIdentityCol }) @@ -218,7 +218,7 @@ function buildMetaTableHelper(Streams.cols) .innerJoin(Streams.meta.name, Streams.meta.col.streamId, Streams.col.id) - .where(Streams.meta.col.key, 'viewerE2eTestStreamVersion') + .where(Streams.meta.col.key, Streams.meta.metaKey.onboardingBaseStream) .andWhereRaw(`${Streams.meta.col.value}::text = ?`, JSON.stringify(version)) .first() diff --git a/packages/server/modules/core/repositories/users.ts b/packages/server/modules/core/repositories/users.ts index baf98de80..37427a3f9 100644 --- a/packages/server/modules/core/repositories/users.ts +++ b/packages/server/modules/core/repositories/users.ts @@ -4,6 +4,7 @@ import { Nullable } from '@/modules/shared/helpers/typeHelper' import { isArray } from 'lodash' import { metaHelpers } from '@/modules/core/helpers/meta' import { UserValidationError } from '@/modules/core/errors/user' +import { Roles } from '@speckle/shared' export type UserWithOptionalRole = User & { /** @@ -134,3 +135,12 @@ export async function updateUser( const [newUser] = await Users.knex().where(Users.col.id, userId).update(update, '*') return newUser as Nullable } + +export async function getFirstAdmin() { + const q = Users.knex() + .select(Users.cols) + .innerJoin(ServerAcl.name, ServerAcl.col.userId, Users.col.id) + .where(ServerAcl.col.role, Roles.Server.Admin) + + return await q.first() +} diff --git a/packages/server/modules/core/services/streams/onboarding.ts b/packages/server/modules/core/services/streams/onboarding.ts index a76d22e30..71f1839de 100644 --- a/packages/server/modules/core/services/streams/onboarding.ts +++ b/packages/server/modules/core/services/streams/onboarding.ts @@ -1,34 +1,28 @@ -import { Nullable, Optional } from '@speckle/shared' -import { getOnboardingStreamId } from '@/modules/shared/helpers/envHelper' +import { Optional } from '@speckle/shared' import { StreamCloneError } from '@/modules/core/errors/stream' import { cloneStream } from '@/modules/core/services/streams/clone' import { StreamRecord } from '@/modules/core/helpers/types' import { logger } from '@/logging/logging' import { createStreamReturnRecord } from '@/modules/core/services/streams/management' - -async function cloneOnboardingStream(userId: string, sourceStreamId: Nullable) { - if (!sourceStreamId) { - throw new StreamCloneError('Onboarding stream ID undefined, check env vars') - } - - return await cloneStream(userId, sourceStreamId) -} +import { getOnboardingBaseProject } from '@/modules/cross-server-sync/services/onboardingProject' export async function createOnboardingStream(targetUserId: string) { - const sourceStreamId = getOnboardingStreamId() + const sourceStream = await getOnboardingBaseProject() - let newStream: Optional = undefined - try { - newStream = await cloneOnboardingStream(targetUserId, sourceStreamId) - } catch (e) { - if (!(e instanceof StreamCloneError)) { - throw e - } else { - logger.warn(e, 'Stream clone failed') + if (sourceStream) { + let newStream: Optional = undefined + try { + newStream = await cloneStream(targetUserId, sourceStream.id) + } catch (e) { + if (!(e instanceof StreamCloneError)) { + throw e + } else { + logger.warn(e, 'Stream clone failed') + } } - } - if (newStream) return newStream + if (newStream) return newStream + } // clone failed, just create empty stream return await createStreamReturnRecord({ ownerId: targetUserId }) diff --git a/packages/server/modules/cross-server-sync/errors/index.ts b/packages/server/modules/cross-server-sync/errors/index.ts new file mode 100644 index 000000000..e36dc5088 --- /dev/null +++ b/packages/server/modules/cross-server-sync/errors/index.ts @@ -0,0 +1,11 @@ +import { BaseError } from '@/modules/shared/errors' + +export class CrossServerCommitSyncError extends BaseError { + static code = 'CROSS_SERVER_COMMIT_SYNC_ERROR' + static defaultMessage = 'Cross-server commit sync failed unexpectedly' +} + +export class CrossServerProjectSyncError extends BaseError { + static code = 'CROSS_SERVER_PROJECT_SYNC_ERROR' + static defaultMessage = 'Cross-server project sync failed unexpectedly' +} diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts new file mode 100644 index 000000000..fd40ead31 --- /dev/null +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -0,0 +1,2689 @@ +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + /** The `BigInt` scalar type represents non-fractional signed whole numeric values. */ + BigInt: bigint; + /** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ + DateTime: string; + EmailAddress: any; + /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ + JSONObject: Record; +}; + +export type ActiveUserMutations = { + __typename?: 'ActiveUserMutations'; + /** Mark onboarding as complete */ + finishOnboarding: Scalars['Boolean']; + /** Edit a user's profile */ + update: User; +}; + + +export type ActiveUserMutationsUpdateArgs = { + user: UserUpdateInput; +}; + +export type Activity = { + __typename?: 'Activity'; + actionType: Scalars['String']; + id: Scalars['ID']; + info: Scalars['JSONObject']; + message: Scalars['String']; + resourceId: Scalars['String']; + resourceType: Scalars['String']; + streamId?: Maybe; + time: Scalars['DateTime']; + userId: Scalars['String']; +}; + +export type ActivityCollection = { + __typename?: 'ActivityCollection'; + cursor?: Maybe; + items?: Maybe>>; + totalCount: Scalars['Int']; +}; + +export type AdminUsersListCollection = { + __typename?: 'AdminUsersListCollection'; + items: Array; + totalCount: Scalars['Int']; +}; + +/** + * A representation of a registered or invited user in the admin users list. Either registeredUser + * or invitedUser will always be set, both values can't be null. + */ +export type AdminUsersListItem = { + __typename?: 'AdminUsersListItem'; + id: Scalars['String']; + invitedUser?: Maybe; + registeredUser?: Maybe; +}; + +export type ApiToken = { + __typename?: 'ApiToken'; + createdAt: Scalars['DateTime']; + id: Scalars['String']; + lastChars: Scalars['String']; + lastUsed: Scalars['DateTime']; + lifespan: Scalars['BigInt']; + name: Scalars['String']; + scopes: Array>; +}; + +export type ApiTokenCreateInput = { + lifespan?: InputMaybe; + name: Scalars['String']; + scopes: Array; +}; + +export type AppAuthor = { + __typename?: 'AppAuthor'; + avatar?: Maybe; + id?: Maybe; + name?: Maybe; +}; + +export type AppCreateInput = { + description: Scalars['String']; + logo?: InputMaybe; + name: Scalars['String']; + public?: InputMaybe; + redirectUrl: Scalars['String']; + scopes: Array>; + termsAndConditionsLink?: InputMaybe; +}; + +export type AppUpdateInput = { + description: Scalars['String']; + id: Scalars['String']; + logo?: InputMaybe; + name: Scalars['String']; + public?: InputMaybe; + redirectUrl: Scalars['String']; + scopes: Array>; + termsAndConditionsLink?: InputMaybe; +}; + +export type AuthStrategy = { + __typename?: 'AuthStrategy'; + color?: Maybe; + icon: Scalars['String']; + id: Scalars['String']; + name: Scalars['String']; + url: Scalars['String']; +}; + +export type BlobMetadata = { + __typename?: 'BlobMetadata'; + createdAt: Scalars['DateTime']; + fileHash?: Maybe; + fileName: Scalars['String']; + fileSize?: Maybe; + fileType: Scalars['String']; + id: Scalars['String']; + streamId: Scalars['String']; + uploadError?: Maybe; + uploadStatus: Scalars['Int']; + userId: Scalars['String']; +}; + +export type BlobMetadataCollection = { + __typename?: 'BlobMetadataCollection'; + cursor?: Maybe; + items?: Maybe>; + totalCount: Scalars['Int']; + totalSize: Scalars['Int']; +}; + +export type Branch = { + __typename?: 'Branch'; + /** All the recent activity on this branch in chronological order */ + activity?: Maybe; + author?: Maybe; + commits?: Maybe; + createdAt?: Maybe; + description?: Maybe; + id: Scalars['String']; + name: Scalars['String']; +}; + + +export type BranchActivityArgs = { + actionType?: InputMaybe; + after?: InputMaybe; + before?: InputMaybe; + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +export type BranchCommitsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + +export type BranchCollection = { + __typename?: 'BranchCollection'; + cursor?: Maybe; + items?: Maybe>; + totalCount: Scalars['Int']; +}; + +export type BranchCreateInput = { + description?: InputMaybe; + name: Scalars['String']; + streamId: Scalars['String']; +}; + +export type BranchDeleteInput = { + id: Scalars['String']; + streamId: Scalars['String']; +}; + +export type BranchUpdateInput = { + description?: InputMaybe; + id: Scalars['String']; + name?: InputMaybe; + streamId: Scalars['String']; +}; + +export type Comment = { + __typename?: 'Comment'; + archived: Scalars['Boolean']; + author: LimitedUser; + authorId: Scalars['String']; + createdAt: Scalars['DateTime']; + /** + * Legacy comment viewer data field + * @deprecated Use the new viewerState field instead + */ + data?: Maybe; + /** Whether or not comment is a reply to another comment */ + hasParent: Scalars['Boolean']; + id: Scalars['String']; + /** Parent thread, if there's any */ + parent?: Maybe; + /** Plain-text version of the comment text, ideal for previews */ + rawText: Scalars['String']; + /** @deprecated Not actually implemented */ + reactions?: Maybe>>; + /** Gets the replies to this comment. */ + replies: CommentCollection; + /** Get authors of replies to this comment */ + replyAuthors: CommentReplyAuthorCollection; + /** Resources that this comment targets. Can be a mixture of either one stream, or multiple commits and objects. */ + resources: Array; + screenshot?: Maybe; + text: SmartTextEditorValue; + /** The time this comment was last updated. Corresponds also to the latest reply to this comment, if any. */ + updatedAt: Scalars['DateTime']; + /** The last time you viewed this comment. Present only if an auth'ed request. Relevant only if a top level commit. */ + viewedAt?: Maybe; + /** Resource identifiers as defined and implemented in the Viewer of the new frontend */ + viewerResources: Array; + /** SerializedViewerState */ + viewerState?: Maybe; +}; + + +export type CommentRepliesArgs = { + cursor?: InputMaybe; + limit?: InputMaybe; +}; + + +export type CommentReplyAuthorsArgs = { + limit?: Scalars['Int']; +}; + +export type CommentActivityMessage = { + __typename?: 'CommentActivityMessage'; + comment: Comment; + type: Scalars['String']; +}; + +export type CommentCollection = { + __typename?: 'CommentCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']; +}; + +export type CommentContentInput = { + blobIds?: InputMaybe>; + doc?: InputMaybe; +}; + +/** Deprecated: Used by old stream-based mutations */ +export type CommentCreateInput = { + /** IDs of uploaded blobs that should be attached to this comment */ + blobIds: Array; + data: Scalars['JSONObject']; + /** + * Specifies the resources this comment is linked to. There are several use cases: + * - a comment targets only one resource (commit or object) + * - a comment targets one or more resources (commits or objects) + * - a comment targets only a stream + */ + resources: Array>; + screenshot?: InputMaybe; + streamId: Scalars['String']; + /** ProseMirror document object */ + text?: InputMaybe; +}; + +export type CommentDataFilters = { + __typename?: 'CommentDataFilters'; + hiddenIds?: Maybe>; + isolatedIds?: Maybe>; + passMax?: Maybe; + passMin?: Maybe; + propertyInfoKey?: Maybe; + sectionBox?: Maybe; +}; + +/** Equivalent to frontend-1's LocalFilterState */ +export type CommentDataFiltersInput = { + hiddenIds?: InputMaybe>; + isolatedIds?: InputMaybe>; + passMax?: InputMaybe; + passMin?: InputMaybe; + propertyInfoKey?: InputMaybe; + sectionBox?: InputMaybe; +}; + +/** Deprecated: Used by old stream-based mutations */ +export type CommentEditInput = { + /** IDs of uploaded blobs that should be attached to this comment */ + blobIds: Array; + id: Scalars['String']; + streamId: Scalars['String']; + /** ProseMirror document object */ + text?: InputMaybe; +}; + +export type CommentMutations = { + __typename?: 'CommentMutations'; + archive: Scalars['Boolean']; + create: Comment; + edit: Comment; + markViewed: Scalars['Boolean']; + reply: Comment; +}; + + +export type CommentMutationsArchiveArgs = { + archived?: Scalars['Boolean']; + commentId: Scalars['String']; +}; + + +export type CommentMutationsCreateArgs = { + input: CreateCommentInput; +}; + + +export type CommentMutationsEditArgs = { + input: EditCommentInput; +}; + + +export type CommentMutationsMarkViewedArgs = { + commentId: Scalars['String']; +}; + + +export type CommentMutationsReplyArgs = { + input: CreateCommentReplyInput; +}; + +export type CommentReplyAuthorCollection = { + __typename?: 'CommentReplyAuthorCollection'; + items: Array; + totalCount: Scalars['Int']; +}; + +export type CommentThreadActivityMessage = { + __typename?: 'CommentThreadActivityMessage'; + data?: Maybe; + reply?: Maybe; + type: Scalars['String']; +}; + +export type Commit = { + __typename?: 'Commit'; + /** All the recent activity on this commit in chronological order */ + activity?: Maybe; + authorAvatar?: Maybe; + authorId?: Maybe; + authorName?: Maybe; + branch?: Maybe; + branchName?: Maybe; + /** + * The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this commit's id. + * E.g., + * ``` + * query{ + * comments(streamId:"streamId" resources:[{resourceType: commit, resourceId:"commitId"}] ){ + * ... + * } + * ``` + */ + commentCount: Scalars['Int']; + createdAt?: Maybe; + id: Scalars['String']; + message?: Maybe; + parents?: Maybe>>; + referencedObject: Scalars['String']; + sourceApplication?: Maybe; + /** + * Will throw an authorization error if active user isn't authorized to see it, for example, + * if a stream isn't public and the user doesn't have the appropriate rights. + */ + stream: Stream; + /** @deprecated Use the stream field instead */ + streamId?: Maybe; + /** @deprecated Use the stream field instead */ + streamName?: Maybe; + totalChildrenCount?: Maybe; +}; + + +export type CommitActivityArgs = { + actionType?: InputMaybe; + after?: InputMaybe; + before?: InputMaybe; + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + +export type CommitCollection = { + __typename?: 'CommitCollection'; + cursor?: Maybe; + items?: Maybe>; + totalCount: Scalars['Int']; +}; + +export type CommitCreateInput = { + branchName: Scalars['String']; + message?: InputMaybe; + objectId: Scalars['String']; + parents?: InputMaybe>>; + /** + * **DEPRECATED** Use the `parents` field. + * @deprecated Field no longer supported + */ + previousCommitIds?: InputMaybe>>; + sourceApplication?: InputMaybe; + streamId: Scalars['String']; + totalChildrenCount?: InputMaybe; +}; + +export type CommitDeleteInput = { + id: Scalars['String']; + streamId: Scalars['String']; +}; + +export type CommitReceivedInput = { + commitId: Scalars['String']; + message?: InputMaybe; + sourceApplication: Scalars['String']; + streamId: Scalars['String']; +}; + +export type CommitUpdateInput = { + id: Scalars['String']; + message?: InputMaybe; + /** To move the commit to a different branch, please the name of the branch. */ + newBranchName?: InputMaybe; + streamId: Scalars['String']; +}; + +export type CommitsDeleteInput = { + commitIds: Array; +}; + +export type CommitsMoveInput = { + commitIds: Array; + targetBranch: Scalars['String']; +}; + +export type CreateCommentInput = { + content: CommentContentInput; + projectId: Scalars['String']; + /** Resources that this comment should be attached to */ + resourceIdString: Scalars['String']; + screenshot?: InputMaybe; + /** + * SerializedViewerState. If omitted, comment won't render (correctly) inside the + * viewer, but will still be retrievable through the API + */ + viewerState?: InputMaybe; +}; + +export type CreateCommentReplyInput = { + content: CommentContentInput; + threadId: Scalars['String']; +}; + +export type CreateModelInput = { + name: Scalars['String']; + projectId: Scalars['ID']; +}; + +export type DeleteModelInput = { + id: Scalars['ID']; + projectId: Scalars['ID']; +}; + +export type DeleteVersionsInput = { + versionIds: Array; +}; + +export enum DiscoverableStreamsSortType { + CreatedDate = 'CREATED_DATE', + FavoritesCount = 'FAVORITES_COUNT' +} + +export type DiscoverableStreamsSortingInput = { + direction: SortDirection; + type: DiscoverableStreamsSortType; +}; + +export type EditCommentInput = { + commentId: Scalars['String']; + content: CommentContentInput; +}; + +export type FileUpload = { + __typename?: 'FileUpload'; + branchName: Scalars['String']; + /** If present, the conversion result is stored in this commit. */ + convertedCommitId?: Maybe; + convertedLastUpdate: Scalars['DateTime']; + /** Holds any errors or info. */ + convertedMessage?: Maybe; + /** 0 = queued, 1 = processing, 2 = success, 3 = error */ + convertedStatus: Scalars['Int']; + /** Alias for convertedCommitId */ + convertedVersionId?: Maybe; + fileName: Scalars['String']; + fileSize: Scalars['Int']; + fileType: Scalars['String']; + id: Scalars['String']; + /** Model associated with the file upload, if it exists already */ + model?: Maybe; + /** Alias for branchName */ + modelName: Scalars['String']; + /** Alias for streamId */ + projectId: Scalars['String']; + streamId: Scalars['String']; + uploadComplete: Scalars['Boolean']; + uploadDate: Scalars['DateTime']; + /** The user's id that uploaded this file. */ + userId: Scalars['String']; +}; + +export type LegacyCommentViewerData = { + __typename?: 'LegacyCommentViewerData'; + /** + * An array representing a user's camera position: + * [camPos.x, camPos.y, camPos.z, camTarget.x, camTarget.y, camTarget.z, isOrtho, zoomNumber] + */ + camPos: Array; + /** Old FE LocalFilterState type */ + filters: CommentDataFilters; + /** THREE.Vector3 {x, y, z} */ + location: Scalars['JSONObject']; + /** Viewer.getCurrentSectionBox(): THREE.Box3 */ + sectionBox?: Maybe; + /** Currently unused. Ideally comments should keep track of selected objects. */ + selection?: Maybe; +}; + +/** + * Limited user type, for showing public info about a user + * to another user + */ +export type LimitedUser = { + __typename?: 'LimitedUser'; + /** All the recent activity from this user in chronological order */ + activity?: Maybe; + avatar?: Maybe; + bio?: Maybe; + /** Get public stream commits authored by the user */ + commits?: Maybe; + company?: Maybe; + id: Scalars['ID']; + name: Scalars['String']; + role?: Maybe; + /** Returns all discoverable streams that the user is a collaborator on */ + streams: StreamCollection; + /** The user's timeline in chronological order */ + timeline?: Maybe; + /** Total amount of favorites attached to streams owned by the user */ + totalOwnedStreamsFavorites: Scalars['Int']; + verified?: Maybe; +}; + + +/** + * Limited user type, for showing public info about a user + * to another user + */ +export type LimitedUserActivityArgs = { + actionType?: InputMaybe; + after?: InputMaybe; + before?: InputMaybe; + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +/** + * Limited user type, for showing public info about a user + * to another user + */ +export type LimitedUserCommitsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +/** + * Limited user type, for showing public info about a user + * to another user + */ +export type LimitedUserStreamsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +/** + * Limited user type, for showing public info about a user + * to another user + */ +export type LimitedUserTimelineArgs = { + after?: InputMaybe; + before?: InputMaybe; + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + +export type Model = { + __typename?: 'Model'; + author: LimitedUser; + /** Return a model tree of children */ + childrenTree: Array; + /** All comment threads in this model */ + commentThreads: CommentCollection; + createdAt: Scalars['DateTime']; + description?: Maybe; + /** The shortened/display name that doesn't include the names of parent models */ + displayName: Scalars['String']; + id: Scalars['ID']; + /** Full name including the names of parent models delimited by forward slashes */ + name: Scalars['String']; + /** Returns a list of versions that are being created from a file import */ + pendingImportedVersions: Array; + previewUrl?: Maybe; + updatedAt: Scalars['DateTime']; + version: Version; + versions: VersionCollection; +}; + + +export type ModelCommentThreadsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +export type ModelPendingImportedVersionsArgs = { + limit?: InputMaybe; +}; + + +export type ModelVersionArgs = { + id: Scalars['String']; +}; + + +export type ModelVersionsArgs = { + cursor?: InputMaybe; + filter?: InputMaybe; + limit?: Scalars['Int']; +}; + +export type ModelCollection = { + __typename?: 'ModelCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']; +}; + +export type ModelMutations = { + __typename?: 'ModelMutations'; + create: Model; + delete: Scalars['Boolean']; + update: Model; +}; + + +export type ModelMutationsCreateArgs = { + input: CreateModelInput; +}; + + +export type ModelMutationsDeleteArgs = { + input: DeleteModelInput; +}; + + +export type ModelMutationsUpdateArgs = { + input: UpdateModelInput; +}; + +export type ModelVersionsFilter = { + /** Make sure these specified versions are always loaded first */ + priorityIds?: InputMaybe>; + /** Only return versions specified in `priorityIds` */ + priorityIdsOnly?: InputMaybe; +}; + +export type ModelsTreeItem = { + __typename?: 'ModelsTreeItem'; + children: Array; + fullName: Scalars['String']; + /** Whether or not this item has nested children models */ + hasChildren: Scalars['Boolean']; + id: Scalars['ID']; + /** + * Nullable cause the item can represent a parent that doesn't actually exist as a model on its own. + * E.g. A model named "foo/bar" is supposed to be a child of "foo" and will be represented as such, + * even if "foo" doesn't exist as its own model. + */ + model?: Maybe; + name: Scalars['String']; + updatedAt: Scalars['DateTime']; +}; + +export type ModelsTreeItemCollection = { + __typename?: 'ModelsTreeItemCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']; +}; + +export type MoveVersionsInput = { + /** If the name references a nonexistant model, it will be created */ + targetModelName: Scalars['String']; + versionIds: Array; +}; + +export type Mutation = { + __typename?: 'Mutation'; + /** The void stares back. */ + _?: Maybe; + /** Various Active User oriented mutations */ + activeUserMutations: ActiveUserMutations; + adminDeleteUser: Scalars['Boolean']; + /** Creates an personal api token. */ + apiTokenCreate: Scalars['String']; + /** Revokes (deletes) an personal api token. */ + apiTokenRevoke: Scalars['Boolean']; + /** Register a new third party application. */ + appCreate: Scalars['String']; + /** Deletes a thirty party application. */ + appDelete: Scalars['Boolean']; + /** Revokes (de-authorizes) an application that you have previously authorized. */ + appRevokeAccess?: Maybe; + /** Update an existing third party application. **Note: This will invalidate all existing tokens, refresh tokens and access codes and will require existing users to re-authorize it.** */ + appUpdate: Scalars['Boolean']; + branchCreate: Scalars['String']; + branchDelete: Scalars['Boolean']; + branchUpdate: Scalars['Boolean']; + /** Broadcast user activity in the viewer */ + broadcastViewerUserActivity: Scalars['Boolean']; + /** + * Archives a comment. + * @deprecated Use commentMutations version + */ + commentArchive: Scalars['Boolean']; + /** + * Creates a comment + * @deprecated Use commentMutations version + */ + commentCreate: Scalars['String']; + /** + * Edits a comment. + * @deprecated Use commentMutations version + */ + commentEdit: Scalars['Boolean']; + commentMutations: CommentMutations; + /** + * Adds a reply to a comment. + * @deprecated Use commentMutations version + */ + commentReply: Scalars['String']; + /** + * Flags a comment as viewed by you (the logged in user). + * @deprecated Use commentMutations version + */ + commentView: Scalars['Boolean']; + commitCreate: Scalars['String']; + commitDelete: Scalars['Boolean']; + commitReceive: Scalars['Boolean']; + commitUpdate: Scalars['Boolean']; + /** Delete a batch of commits */ + commitsDelete: Scalars['Boolean']; + /** Move a batch of commits to a new branch */ + commitsMove: Scalars['Boolean']; + /** Delete a pending invite */ + inviteDelete: Scalars['Boolean']; + /** Re-send a pending invite */ + inviteResend: Scalars['Boolean']; + modelMutations: ModelMutations; + objectCreate: Array>; + projectMutations: ProjectMutations; + /** (Re-)send the account verification e-mail */ + requestVerification: Scalars['Boolean']; + serverInfoUpdate?: Maybe; + serverInviteBatchCreate: Scalars['Boolean']; + /** Invite a new user to the speckle server and return the invite ID */ + serverInviteCreate: Scalars['Boolean']; + /** Request access to a specific stream */ + streamAccessRequestCreate: StreamAccessRequest; + /** Accept or decline a stream access request. Must be a stream owner to invoke this. */ + streamAccessRequestUse: Scalars['Boolean']; + /** Creates a new stream. */ + streamCreate?: Maybe; + /** Deletes an existing stream. */ + streamDelete: Scalars['Boolean']; + streamFavorite?: Maybe; + streamInviteBatchCreate: Scalars['Boolean']; + /** Cancel a pending stream invite. Can only be invoked by a stream owner. */ + streamInviteCancel: Scalars['Boolean']; + /** Invite a new or registered user to the specified stream */ + streamInviteCreate: Scalars['Boolean']; + /** Accept or decline a stream invite */ + streamInviteUse: Scalars['Boolean']; + /** Remove yourself from stream collaborators (not possible for the owner) */ + streamLeave: Scalars['Boolean']; + /** Revokes the permissions of a user on a given stream. */ + streamRevokePermission?: Maybe; + /** Updates an existing stream. */ + streamUpdate: Scalars['Boolean']; + /** Update permissions of a user on a given stream. */ + streamUpdatePermission?: Maybe; + streamsDelete: Scalars['Boolean']; + /** + * Used for broadcasting real time typing status in comment threads. Does not persist any info. + * @deprecated Use broadcastViewerUserActivity + */ + userCommentThreadActivityBroadcast: Scalars['Boolean']; + /** Delete a user's account. */ + userDelete: Scalars['Boolean']; + userNotificationPreferencesUpdate?: Maybe; + userRoleChange: Scalars['Boolean']; + /** + * Edits a user's profile. + * @deprecated Use activeUserMutations version + */ + userUpdate: Scalars['Boolean']; + /** + * Used for broadcasting real time chat head bubbles and status. Does not persist any info. + * @deprecated Use broadcastViewerUserActivity + */ + userViewerActivityBroadcast: Scalars['Boolean']; + versionMutations: VersionMutations; + /** Creates a new webhook on a stream */ + webhookCreate: Scalars['String']; + /** Deletes an existing webhook */ + webhookDelete: Scalars['String']; + /** Updates an existing webhook */ + webhookUpdate: Scalars['String']; +}; + + +export type MutationAdminDeleteUserArgs = { + userConfirmation: UserDeleteInput; +}; + + +export type MutationApiTokenCreateArgs = { + token: ApiTokenCreateInput; +}; + + +export type MutationApiTokenRevokeArgs = { + token: Scalars['String']; +}; + + +export type MutationAppCreateArgs = { + app: AppCreateInput; +}; + + +export type MutationAppDeleteArgs = { + appId: Scalars['String']; +}; + + +export type MutationAppRevokeAccessArgs = { + appId: Scalars['String']; +}; + + +export type MutationAppUpdateArgs = { + app: AppUpdateInput; +}; + + +export type MutationBranchCreateArgs = { + branch: BranchCreateInput; +}; + + +export type MutationBranchDeleteArgs = { + branch: BranchDeleteInput; +}; + + +export type MutationBranchUpdateArgs = { + branch: BranchUpdateInput; +}; + + +export type MutationBroadcastViewerUserActivityArgs = { + message: ViewerUserActivityMessageInput; + projectId: Scalars['String']; + resourceIdString: Scalars['String']; +}; + + +export type MutationCommentArchiveArgs = { + archived?: Scalars['Boolean']; + commentId: Scalars['String']; + streamId: Scalars['String']; +}; + + +export type MutationCommentCreateArgs = { + input: CommentCreateInput; +}; + + +export type MutationCommentEditArgs = { + input: CommentEditInput; +}; + + +export type MutationCommentReplyArgs = { + input: ReplyCreateInput; +}; + + +export type MutationCommentViewArgs = { + commentId: Scalars['String']; + streamId: Scalars['String']; +}; + + +export type MutationCommitCreateArgs = { + commit: CommitCreateInput; +}; + + +export type MutationCommitDeleteArgs = { + commit: CommitDeleteInput; +}; + + +export type MutationCommitReceiveArgs = { + input: CommitReceivedInput; +}; + + +export type MutationCommitUpdateArgs = { + commit: CommitUpdateInput; +}; + + +export type MutationCommitsDeleteArgs = { + input: CommitsDeleteInput; +}; + + +export type MutationCommitsMoveArgs = { + input: CommitsMoveInput; +}; + + +export type MutationInviteDeleteArgs = { + inviteId: Scalars['String']; +}; + + +export type MutationInviteResendArgs = { + inviteId: Scalars['String']; +}; + + +export type MutationObjectCreateArgs = { + objectInput: ObjectCreateInput; +}; + + +export type MutationServerInfoUpdateArgs = { + info: ServerInfoUpdateInput; +}; + + +export type MutationServerInviteBatchCreateArgs = { + input: Array; +}; + + +export type MutationServerInviteCreateArgs = { + input: ServerInviteCreateInput; +}; + + +export type MutationStreamAccessRequestCreateArgs = { + streamId: Scalars['String']; +}; + + +export type MutationStreamAccessRequestUseArgs = { + accept: Scalars['Boolean']; + requestId: Scalars['String']; + role?: StreamRole; +}; + + +export type MutationStreamCreateArgs = { + stream: StreamCreateInput; +}; + + +export type MutationStreamDeleteArgs = { + id: Scalars['String']; +}; + + +export type MutationStreamFavoriteArgs = { + favorited: Scalars['Boolean']; + streamId: Scalars['String']; +}; + + +export type MutationStreamInviteBatchCreateArgs = { + input: Array; +}; + + +export type MutationStreamInviteCancelArgs = { + inviteId: Scalars['String']; + streamId: Scalars['String']; +}; + + +export type MutationStreamInviteCreateArgs = { + input: StreamInviteCreateInput; +}; + + +export type MutationStreamInviteUseArgs = { + accept: Scalars['Boolean']; + streamId: Scalars['String']; + token: Scalars['String']; +}; + + +export type MutationStreamLeaveArgs = { + streamId: Scalars['String']; +}; + + +export type MutationStreamRevokePermissionArgs = { + permissionParams: StreamRevokePermissionInput; +}; + + +export type MutationStreamUpdateArgs = { + stream: StreamUpdateInput; +}; + + +export type MutationStreamUpdatePermissionArgs = { + permissionParams: StreamUpdatePermissionInput; +}; + + +export type MutationStreamsDeleteArgs = { + ids?: InputMaybe>; +}; + + +export type MutationUserCommentThreadActivityBroadcastArgs = { + commentId: Scalars['String']; + data?: InputMaybe; + streamId: Scalars['String']; +}; + + +export type MutationUserDeleteArgs = { + userConfirmation: UserDeleteInput; +}; + + +export type MutationUserNotificationPreferencesUpdateArgs = { + preferences: Scalars['JSONObject']; +}; + + +export type MutationUserRoleChangeArgs = { + userRoleInput: UserRoleInput; +}; + + +export type MutationUserUpdateArgs = { + user: UserUpdateInput; +}; + + +export type MutationUserViewerActivityBroadcastArgs = { + data?: InputMaybe; + resourceId: Scalars['String']; + streamId: Scalars['String']; +}; + + +export type MutationWebhookCreateArgs = { + webhook: WebhookCreateInput; +}; + + +export type MutationWebhookDeleteArgs = { + webhook: WebhookDeleteInput; +}; + + +export type MutationWebhookUpdateArgs = { + webhook: WebhookUpdateInput; +}; + +export type Object = { + __typename?: 'Object'; + applicationId?: Maybe; + /** + * Get any objects that this object references. In the case of commits, this will give you a commit's constituent objects. + * **NOTE**: Providing any of the two last arguments ( `query`, `orderBy` ) will trigger a different code branch that executes a much more expensive SQL query. It is not recommended to do so for basic clients that are interested in purely getting all the objects of a given commit. + */ + children: ObjectCollection; + /** + * The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this object's id. + * E.g., + * ``` + * query{ + * comments(streamId:"streamId" resources:[{resourceType: object, resourceId:"objectId"}] ){ + * ... + * } + * ``` + */ + commentCount: Scalars['Int']; + createdAt?: Maybe; + /** The full object, with all its props & other things. **NOTE:** If you're requesting objects for the purpose of recreating & displaying, you probably only want to request this specific field. */ + data?: Maybe; + id: Scalars['String']; + speckleType?: Maybe; + totalChildrenCount?: Maybe; +}; + + +export type ObjectChildrenArgs = { + cursor?: InputMaybe; + depth?: Scalars['Int']; + limit?: Scalars['Int']; + orderBy?: InputMaybe; + query?: InputMaybe>; + select?: InputMaybe>>; +}; + +export type ObjectCollection = { + __typename?: 'ObjectCollection'; + cursor?: Maybe; + objects: Array>; + totalCount: Scalars['Int']; +}; + +export type ObjectCreateInput = { + /** The objects you want to create. */ + objects: Array>; + /** The stream against which these objects will be created. */ + streamId: Scalars['String']; +}; + +export type PasswordStrengthCheckFeedback = { + __typename?: 'PasswordStrengthCheckFeedback'; + suggestions: Array; + warning?: Maybe; +}; + +export type PasswordStrengthCheckResults = { + __typename?: 'PasswordStrengthCheckResults'; + /** Verbal feedback to help choose better passwords. set when score <= 2. */ + feedback: PasswordStrengthCheckFeedback; + /** + * Integer from 0-4 (useful for implementing a strength bar): + * 0 too guessable: risky password. (guesses < 10^3) + * 1 very guessable: protection from throttled online attacks. (guesses < 10^6) + * 2 somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8) + * 3 safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10) + * 4 very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10) + */ + score: Scalars['Int']; +}; + +export type PendingStreamCollaborator = { + __typename?: 'PendingStreamCollaborator'; + id: Scalars['String']; + inviteId: Scalars['String']; + invitedBy: LimitedUser; + projectId: Scalars['String']; + projectName: Scalars['String']; + role: Scalars['String']; + streamId: Scalars['String']; + streamName: Scalars['String']; + /** E-mail address or name of the invited user */ + title: Scalars['String']; + /** Only available if the active user is the pending stream collaborator */ + token?: Maybe; + /** Set only if user is registered */ + user?: Maybe; +}; + +export type Project = { + __typename?: 'Project'; + allowPublicComments: Scalars['Boolean']; + /** All comment threads in this project */ + commentThreads: ProjectCommentCollection; + createdAt: Scalars['DateTime']; + description?: Maybe; + id: Scalars['ID']; + /** Collaborators who have been invited, but not yet accepted. */ + invitedTeam?: Maybe>; + /** Returns a specific model by its ID */ + model: Model; + /** Return a model tree of children for the specified model name */ + modelChildrenTree: Array; + /** Returns a flat list of all models */ + models: ModelCollection; + /** + * Return's a project's models in a tree view with submodels being nested under parent models + * real or fake (e.g., with a foo/bar model, it will be nested under foo even if such a model doesn't actually exist) + */ + modelsTree: ModelsTreeItemCollection; + name: Scalars['String']; + /** Returns a list models that are being created from a file import */ + pendingImportedModels: Array; + /** Active user's role for this project. `null` if request is not authenticated, or the project is not explicitly shared with you. */ + role?: Maybe; + /** Source apps used in any models of this project */ + sourceApps: Array; + team: Array; + updatedAt: Scalars['DateTime']; + /** Returns a flat list of all project versions */ + versions: VersionCollection; + /** Return metadata about resources being requested in the viewer */ + viewerResources: Array; + visibility: ProjectVisibility; +}; + + +export type ProjectCommentThreadsArgs = { + cursor?: InputMaybe; + filter?: InputMaybe; + limit?: Scalars['Int']; +}; + + +export type ProjectModelArgs = { + id: Scalars['String']; +}; + + +export type ProjectModelChildrenTreeArgs = { + fullName: Scalars['String']; +}; + + +export type ProjectModelsArgs = { + cursor?: InputMaybe; + filter?: InputMaybe; + limit?: Scalars['Int']; +}; + + +export type ProjectModelsTreeArgs = { + cursor?: InputMaybe; + filter?: InputMaybe; + limit?: Scalars['Int']; +}; + + +export type ProjectPendingImportedModelsArgs = { + limit?: InputMaybe; +}; + + +export type ProjectVersionsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +export type ProjectViewerResourcesArgs = { + loadedVersionsOnly?: InputMaybe; + resourceIdString: Scalars['String']; +}; + +export type ProjectCollaborator = { + __typename?: 'ProjectCollaborator'; + role: Scalars['String']; + user: LimitedUser; +}; + +export type ProjectCollection = { + __typename?: 'ProjectCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']; +}; + +export type ProjectCommentCollection = { + __typename?: 'ProjectCommentCollection'; + cursor?: Maybe; + items: Array; + totalArchivedCount: Scalars['Int']; + totalCount: Scalars['Int']; +}; + +export type ProjectCommentsFilter = { + /** Whether or not to include archived/resolved threads */ + includeArchived?: InputMaybe; + /** + * By default if resourceIdString is set, the "versionId" part of model resource identifiers will be ignored + * and all comments of all versions of any of the referenced models will be returned. If `loadedVersionsOnly` is + * enabled, then only comment threads of loaded/referenced versions in resourceIdString will be returned. + */ + loadedVersionsOnly?: InputMaybe; + /** + * Only request comments belonging to the resources identified by this + * comma-delimited resouce string (same format that's used in the viewer URL) + */ + resourceIdString?: InputMaybe; +}; + +export type ProjectCommentsUpdatedMessage = { + __typename?: 'ProjectCommentsUpdatedMessage'; + /** Null if deleted */ + comment?: Maybe; + id: Scalars['String']; + type: ProjectCommentsUpdatedMessageType; +}; + +export enum ProjectCommentsUpdatedMessageType { + Archived = 'ARCHIVED', + Created = 'CREATED', + Updated = 'UPDATED' +} + +/** Any values left null will be ignored */ +export type ProjectCreateInput = { + description?: InputMaybe; + name?: InputMaybe; + visibility?: InputMaybe; +}; + +export type ProjectInviteCreateInput = { + /** Either this or userId must be filled */ + email?: InputMaybe; + /** Defaults to the contributor role, if not specified */ + role?: InputMaybe; + /** Either this or email must be filled */ + userId?: InputMaybe; +}; + +export type ProjectInviteMutations = { + __typename?: 'ProjectInviteMutations'; + /** Batch invite to project */ + batchCreate: Project; + /** Cancel a pending stream invite. Can only be invoked by a project owner. */ + cancel: Project; + /** Invite a new or registered user to be a project collaborator. Can only be invoked by a project owner. */ + create: Project; + /** Accept or decline a project invite */ + use: Scalars['Boolean']; +}; + + +export type ProjectInviteMutationsBatchCreateArgs = { + input: Array; + projectId: Scalars['ID']; +}; + + +export type ProjectInviteMutationsCancelArgs = { + inviteId: Scalars['String']; + projectId: Scalars['ID']; +}; + + +export type ProjectInviteMutationsCreateArgs = { + input: ProjectInviteCreateInput; + projectId: Scalars['ID']; +}; + + +export type ProjectInviteMutationsUseArgs = { + input: ProjectInviteUseInput; +}; + +export type ProjectInviteUseInput = { + accept: Scalars['Boolean']; + projectId: Scalars['ID']; + token: Scalars['String']; +}; + +export type ProjectModelsFilter = { + /** Filter by IDs of contributors who participated in models */ + contributors?: InputMaybe>; + /** Excldue models w/ the specified IDs */ + excludeIds?: InputMaybe>; + /** Only select models w/ the specified IDs */ + ids?: InputMaybe>; + /** Filter out models that don't have any versions */ + onlyWithVersions?: InputMaybe; + /** Filter by model names */ + search?: InputMaybe; + /** Filter by source apps used in models */ + sourceApps?: InputMaybe>; +}; + +export type ProjectModelsTreeFilter = { + /** Filter by IDs of contributors who participated in models */ + contributors?: InputMaybe>; + /** Search for specific models. If used, tree items from different levels may be mixed. */ + search?: InputMaybe; + /** Filter by source apps used in models */ + sourceApps?: InputMaybe>; +}; + +export type ProjectModelsUpdatedMessage = { + __typename?: 'ProjectModelsUpdatedMessage'; + /** Model ID */ + id: Scalars['String']; + /** Null if model was deleted */ + model?: Maybe; + type: ProjectModelsUpdatedMessageType; +}; + +export enum ProjectModelsUpdatedMessageType { + Created = 'CREATED', + Deleted = 'DELETED', + Updated = 'UPDATED' +} + +export type ProjectMutations = { + __typename?: 'ProjectMutations'; + /** Create new project */ + create: Project; + /** Create onboarding/tutorial project */ + createForOnboarding: Project; + /** Delete an existing project */ + delete: Scalars['Boolean']; + /** Invite related mutations */ + invites: ProjectInviteMutations; + /** Leave a project. Only possible if you're not the last remaining owner. */ + leave: Scalars['Boolean']; + /** Updates an existing project */ + update: Project; + /** Update role for a collaborator */ + updateRole: Project; +}; + + +export type ProjectMutationsCreateArgs = { + input?: InputMaybe; +}; + + +export type ProjectMutationsDeleteArgs = { + id: Scalars['String']; +}; + + +export type ProjectMutationsLeaveArgs = { + id: Scalars['String']; +}; + + +export type ProjectMutationsUpdateArgs = { + update: ProjectUpdateInput; +}; + + +export type ProjectMutationsUpdateRoleArgs = { + input: ProjectUpdateRoleInput; +}; + +export type ProjectPendingModelsUpdatedMessage = { + __typename?: 'ProjectPendingModelsUpdatedMessage'; + /** Upload ID */ + id: Scalars['String']; + model: FileUpload; + type: ProjectPendingModelsUpdatedMessageType; +}; + +export enum ProjectPendingModelsUpdatedMessageType { + Created = 'CREATED', + Updated = 'UPDATED' +} + +export type ProjectPendingVersionsUpdatedMessage = { + __typename?: 'ProjectPendingVersionsUpdatedMessage'; + /** Upload ID */ + id: Scalars['String']; + type: ProjectPendingVersionsUpdatedMessageType; + version: FileUpload; +}; + +export enum ProjectPendingVersionsUpdatedMessageType { + Created = 'CREATED', + Updated = 'UPDATED' +} + +/** Any values left null will be ignored, so only set the properties that you want updated */ +export type ProjectUpdateInput = { + allowPublicComments?: InputMaybe; + description?: InputMaybe; + id: Scalars['ID']; + name?: InputMaybe; + visibility?: InputMaybe; +}; + +export type ProjectUpdateRoleInput = { + projectId: Scalars['String']; + /** Leave role as null to revoke access entirely */ + role?: InputMaybe; + userId: Scalars['String']; +}; + +export type ProjectUpdatedMessage = { + __typename?: 'ProjectUpdatedMessage'; + /** Project ID */ + id: Scalars['String']; + /** Project entity, null if project was deleted */ + project?: Maybe; + /** Message type */ + type: ProjectUpdatedMessageType; +}; + +export enum ProjectUpdatedMessageType { + Deleted = 'DELETED', + Updated = 'UPDATED' +} + +export type ProjectVersionsPreviewGeneratedMessage = { + __typename?: 'ProjectVersionsPreviewGeneratedMessage'; + objectId: Scalars['String']; + projectId: Scalars['String']; + versionId: Scalars['String']; +}; + +export type ProjectVersionsUpdatedMessage = { + __typename?: 'ProjectVersionsUpdatedMessage'; + /** Version ID */ + id: Scalars['String']; + /** Only set if version was deleted, in other scenarios can be queried from 'version' */ + modelId?: Maybe; + type: ProjectVersionsUpdatedMessageType; + /** Null if version was deleted */ + version?: Maybe; +}; + +export enum ProjectVersionsUpdatedMessageType { + Created = 'CREATED', + Deleted = 'DELETED', + Updated = 'UPDATED' +} + +export enum ProjectVisibility { + Private = 'PRIVATE', + Public = 'PUBLIC', + Unlisted = 'UNLISTED' +} + +export type Query = { + __typename?: 'Query'; + /** Stare into the void. */ + _?: Maybe; + /** Gets the profile of the authenticated user or null if not authenticated */ + activeUser?: Maybe; + /** All the streams of the server. Available to admins only. */ + adminStreams?: Maybe; + /** + * Get all (or search for specific) users, registered or invited, from the server in a paginated view. + * The query looks for matches in name, company and email. + */ + adminUsers?: Maybe; + /** Gets a specific app from the server. */ + app?: Maybe; + /** Returns all the publicly available apps on this server. */ + apps?: Maybe>>; + comment?: Maybe; + /** + * This query can be used in the following ways: + * - get all the comments for a stream: **do not pass in any resource identifiers**. + * - get the comments targeting any of a set of provided resources (comments/objects): **pass in an array of resources.** + * @deprecated Use 'commentThreads' fields instead + */ + comments?: Maybe; + /** All of the discoverable streams of the server */ + discoverableStreams?: Maybe; + /** Get the (limited) profile information of another server user */ + otherUser?: Maybe; + /** + * Find a specific project. Will throw an authorization error if active user isn't authorized + * to see it, for example, if a project isn't public and the user doesn't have the appropriate rights. + */ + project: Project; + /** + * Look for an invitation to a project, for the current user (authed or not). If token + * isn't specified, the server will look for any valid invite. + */ + projectInvite?: Maybe; + serverInfo: ServerInfo; + serverStats: ServerStats; + /** + * Returns a specific stream. Will throw an authorization error if active user isn't authorized + * to see it, for example, if a stream isn't public and the user doesn't have the appropriate rights. + */ + stream?: Maybe; + /** Get authed user's stream access request */ + streamAccessRequest?: Maybe; + /** + * Look for an invitation to a stream, for the current user (authed or not). If token + * isn't specified, the server will look for any valid invite. + */ + streamInvite?: Maybe; + /** Get all invitations to streams that the active user has */ + streamInvites: Array; + /** + * Returns all streams that the active user is a collaborator on. + * Pass in the `query` parameter to search by name, description or ID. + */ + streams?: Maybe; + testList: Array; + testNumber?: Maybe; + /** + * Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header). + * @deprecated To be removed in the near future! Use 'activeUser' to get info about the active user or 'otherUser' to get info about another user. + */ + user?: Maybe; + /** Validate password strength */ + userPwdStrength: PasswordStrengthCheckResults; + /** + * Search for users and return limited metadata about them, if you have the server:user role. + * The query looks for matches in name & email + */ + userSearch: UserSearchResultCollection; +}; + + +export type QueryAdminStreamsArgs = { + limit?: InputMaybe; + offset?: InputMaybe; + orderBy?: InputMaybe; + query?: InputMaybe; + visibility?: InputMaybe; +}; + + +export type QueryAdminUsersArgs = { + limit?: Scalars['Int']; + offset?: Scalars['Int']; + query?: InputMaybe; +}; + + +export type QueryAppArgs = { + id: Scalars['String']; +}; + + +export type QueryCommentArgs = { + id: Scalars['String']; + streamId: Scalars['String']; +}; + + +export type QueryCommentsArgs = { + archived?: Scalars['Boolean']; + cursor?: InputMaybe; + limit?: InputMaybe; + resources?: InputMaybe>>; + streamId: Scalars['String']; +}; + + +export type QueryDiscoverableStreamsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; + sort?: InputMaybe; +}; + + +export type QueryOtherUserArgs = { + id: Scalars['String']; +}; + + +export type QueryProjectArgs = { + id: Scalars['String']; +}; + + +export type QueryProjectInviteArgs = { + projectId: Scalars['String']; + token?: InputMaybe; +}; + + +export type QueryStreamArgs = { + id: Scalars['String']; +}; + + +export type QueryStreamAccessRequestArgs = { + streamId: Scalars['String']; +}; + + +export type QueryStreamInviteArgs = { + streamId: Scalars['String']; + token?: InputMaybe; +}; + + +export type QueryStreamsArgs = { + cursor?: InputMaybe; + limit?: InputMaybe; + query?: InputMaybe; +}; + + +export type QueryUserArgs = { + id?: InputMaybe; +}; + + +export type QueryUserPwdStrengthArgs = { + pwd: Scalars['String']; +}; + + +export type QueryUserSearchArgs = { + archived?: InputMaybe; + cursor?: InputMaybe; + emailOnly?: InputMaybe; + limit?: Scalars['Int']; + query: Scalars['String']; +}; + +/** Deprecated: Used by old stream-based mutations */ +export type ReplyCreateInput = { + /** IDs of uploaded blobs that should be attached to this reply */ + blobIds: Array; + data?: InputMaybe; + parentComment: Scalars['String']; + streamId: Scalars['String']; + /** ProseMirror document object */ + text?: InputMaybe; +}; + +export type ResourceIdentifier = { + __typename?: 'ResourceIdentifier'; + resourceId: Scalars['String']; + resourceType: ResourceType; +}; + +export type ResourceIdentifierInput = { + resourceId: Scalars['String']; + resourceType: ResourceType; +}; + +export enum ResourceType { + Comment = 'comment', + Commit = 'commit', + Object = 'object', + Stream = 'stream' +} + +/** Available roles. */ +export type Role = { + __typename?: 'Role'; + description: Scalars['String']; + name: Scalars['String']; + resourceTarget: Scalars['String']; +}; + +/** Available scopes. */ +export type Scope = { + __typename?: 'Scope'; + description: Scalars['String']; + name: Scalars['String']; +}; + +export type ServerApp = { + __typename?: 'ServerApp'; + author?: Maybe; + createdAt: Scalars['DateTime']; + description?: Maybe; + id: Scalars['String']; + logo?: Maybe; + name: Scalars['String']; + public?: Maybe; + redirectUrl: Scalars['String']; + scopes: Array; + secret?: Maybe; + termsAndConditionsLink?: Maybe; + trustByDefault?: Maybe; +}; + +export type ServerAppListItem = { + __typename?: 'ServerAppListItem'; + author?: Maybe; + description?: Maybe; + id: Scalars['String']; + logo?: Maybe; + name: Scalars['String']; + redirectUrl: Scalars['String']; + termsAndConditionsLink?: Maybe; + trustByDefault?: Maybe; +}; + +/** Information about this server. */ +export type ServerInfo = { + __typename?: 'ServerInfo'; + adminContact?: Maybe; + /** The authentication strategies available on this server. */ + authStrategies: Array; + blobSizeLimitBytes: Scalars['Int']; + canonicalUrl?: Maybe; + company?: Maybe; + description?: Maybe; + inviteOnly?: Maybe; + name: Scalars['String']; + roles: Array>; + scopes: Array>; + termsOfService?: Maybe; + version?: Maybe; +}; + +export type ServerInfoUpdateInput = { + adminContact?: InputMaybe; + company?: InputMaybe; + description?: InputMaybe; + inviteOnly?: InputMaybe; + name: Scalars['String']; + termsOfService?: InputMaybe; +}; + +export type ServerInvite = { + __typename?: 'ServerInvite'; + email: Scalars['String']; + id: Scalars['String']; + invitedBy: LimitedUser; +}; + +export type ServerInviteCreateInput = { + email: Scalars['String']; + message?: InputMaybe; +}; + +export enum ServerRole { + ServerAdmin = 'SERVER_ADMIN', + ServerArchivedUser = 'SERVER_ARCHIVED_USER', + ServerUser = 'SERVER_USER' +} + +export type ServerStats = { + __typename?: 'ServerStats'; + /** An array of objects currently structured as { created_month: Date, count: int }. */ + commitHistory?: Maybe>>; + /** An array of objects currently structured as { created_month: Date, count: int }. */ + objectHistory?: Maybe>>; + /** An array of objects currently structured as { created_month: Date, count: int }. */ + streamHistory?: Maybe>>; + totalCommitCount: Scalars['Int']; + totalObjectCount: Scalars['Int']; + totalStreamCount: Scalars['Int']; + totalUserCount: Scalars['Int']; + /** An array of objects currently structured as { created_month: Date, count: int }. */ + userHistory?: Maybe>>; +}; + +export type SmartTextEditorValue = { + __typename?: 'SmartTextEditorValue'; + /** File attachments, if any */ + attachments?: Maybe>; + /** + * The actual (ProseMirror) document representing the text. Can be empty, + * if there are attachments. + */ + doc?: Maybe; + /** The type of editor value (comment, blog post etc.) */ + type: Scalars['String']; + /** The version of the schema */ + version: Scalars['String']; +}; + +export enum SortDirection { + Asc = 'ASC', + Desc = 'DESC' +} + +export type Stream = { + __typename?: 'Stream'; + /** All the recent activity on this stream in chronological order */ + activity?: Maybe; + allowPublicComments: Scalars['Boolean']; + blob?: Maybe; + /** Get the metadata collection of blobs stored for this stream. */ + blobs?: Maybe; + branch?: Maybe; + branches?: Maybe; + collaborators: Array; + /** + * The total number of comments for this stream. To actually get the comments, use the comments query without passing in a resource array. E.g.: + * + * ``` + * query{ + * comments(streamId:"streamId"){ + * ... + * } + * ``` + */ + commentCount: Scalars['Int']; + commit?: Maybe; + commits?: Maybe; + createdAt: Scalars['DateTime']; + description?: Maybe; + /** Date when you favorited this stream. `null` if stream isn't viewed from a specific user's perspective or if it isn't favorited. */ + favoritedDate?: Maybe; + favoritesCount: Scalars['Int']; + /** Returns a specific file upload that belongs to this stream. */ + fileUpload?: Maybe; + /** Returns a list of all the file uploads for this stream. */ + fileUploads: Array; + id: Scalars['String']; + /** + * Whether the stream (if public) can be found on public stream exploration pages + * and searches + */ + isDiscoverable: Scalars['Boolean']; + /** Whether the stream can be viewed by non-contributors */ + isPublic: Scalars['Boolean']; + name: Scalars['String']; + object?: Maybe; + /** Pending stream access requests */ + pendingAccessRequests?: Maybe>; + /** Collaborators who have been invited, but not yet accepted. */ + pendingCollaborators?: Maybe>; + /** Your role for this stream. `null` if request is not authenticated, or the stream is not explicitly shared with you. */ + role?: Maybe; + size?: Maybe; + updatedAt: Scalars['DateTime']; + webhooks?: Maybe; +}; + + +export type StreamActivityArgs = { + actionType?: InputMaybe; + after?: InputMaybe; + before?: InputMaybe; + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +export type StreamBlobArgs = { + id: Scalars['String']; +}; + + +export type StreamBlobsArgs = { + cursor?: InputMaybe; + limit?: InputMaybe; + query?: InputMaybe; +}; + + +export type StreamBranchArgs = { + name?: InputMaybe; +}; + + +export type StreamBranchesArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +export type StreamCommitArgs = { + id?: InputMaybe; +}; + + +export type StreamCommitsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +export type StreamFileUploadArgs = { + id: Scalars['String']; +}; + + +export type StreamObjectArgs = { + id: Scalars['String']; +}; + + +export type StreamWebhooksArgs = { + id?: InputMaybe; +}; + +/** Created when a user requests to become a contributor on a stream */ +export type StreamAccessRequest = { + __typename?: 'StreamAccessRequest'; + createdAt: Scalars['DateTime']; + id: Scalars['ID']; + requester: LimitedUser; + requesterId: Scalars['String']; + /** Can only be selected if authed user has proper access */ + stream: Stream; + streamId: Scalars['String']; +}; + +export type StreamCollaborator = { + __typename?: 'StreamCollaborator'; + avatar?: Maybe; + company?: Maybe; + id: Scalars['String']; + name: Scalars['String']; + role: Scalars['String']; +}; + +export type StreamCollection = { + __typename?: 'StreamCollection'; + cursor?: Maybe; + items?: Maybe>; + totalCount: Scalars['Int']; +}; + +export type StreamCreateInput = { + description?: InputMaybe; + /** + * Whether the stream (if public) can be found on public stream exploration pages + * and searches + */ + isDiscoverable?: InputMaybe; + /** Whether the stream can be viewed by non-contributors */ + isPublic?: InputMaybe; + name?: InputMaybe; + /** Optionally specify user IDs of users that you want to invite to be contributors to this stream */ + withContributors?: InputMaybe>; +}; + +export type StreamInviteCreateInput = { + email?: InputMaybe; + message?: InputMaybe; + /** Defaults to the contributor role, if not specified */ + role?: InputMaybe; + streamId: Scalars['String']; + userId?: InputMaybe; +}; + +export type StreamRevokePermissionInput = { + streamId: Scalars['String']; + userId: Scalars['String']; +}; + +export enum StreamRole { + StreamContributor = 'STREAM_CONTRIBUTOR', + StreamOwner = 'STREAM_OWNER', + StreamReviewer = 'STREAM_REVIEWER' +} + +export type StreamUpdateInput = { + allowPublicComments?: InputMaybe; + description?: InputMaybe; + id: Scalars['String']; + /** + * Whether the stream (if public) can be found on public stream exploration pages + * and searches + */ + isDiscoverable?: InputMaybe; + /** Whether the stream can be viewed by non-contributors */ + isPublic?: InputMaybe; + name?: InputMaybe; +}; + +export type StreamUpdatePermissionInput = { + role: Scalars['String']; + streamId: Scalars['String']; + userId: Scalars['String']; +}; + +export type Subscription = { + __typename?: 'Subscription'; + /** It's lonely in the void. */ + _?: Maybe; + /** Subscribe to branch created event */ + branchCreated?: Maybe; + /** Subscribe to branch deleted event */ + branchDeleted?: Maybe; + /** Subscribe to branch updated event. */ + branchUpdated?: Maybe; + /** + * Subscribe to new comment events. There's two ways to use this subscription: + * - for a whole stream: do not pass in any resourceIds; this sub will get called whenever a comment (not reply) is added to any of the stream's resources. + * - for a specific resource/set of resources: pass in a list of resourceIds (commit or object ids); this sub will get called when *any* of the resources provided get a comment. + * @deprecated Use projectCommentsUpdated + */ + commentActivity: CommentActivityMessage; + /** + * Subscribes to events on a specific comment. Use to find out when: + * - a top level comment is deleted (trigger a deletion event outside) + * - a top level comment receives a reply. + * @deprecated Use projectCommentsUpdated or viewerUserActivityBroadcasted for reply status + */ + commentThreadActivity: CommentThreadActivityMessage; + /** Subscribe to commit created event */ + commitCreated?: Maybe; + /** Subscribe to commit deleted event */ + commitDeleted?: Maybe; + /** Subscribe to commit updated event. */ + commitUpdated?: Maybe; + /** + * Subscribe to updates to resource comments/threads. Optionally specify resource ID string to only receive + * updates regarding comments for those resources. + */ + projectCommentsUpdated: ProjectCommentsUpdatedMessage; + /** Subscribe to changes to a project's models. Optionally specify modelIds to track. */ + projectModelsUpdated: ProjectModelsUpdatedMessage; + /** Subscribe to changes to a project's pending models */ + projectPendingModelsUpdated: ProjectPendingModelsUpdatedMessage; + /** Subscribe to changes to a project's pending versions */ + projectPendingVersionsUpdated: ProjectPendingVersionsUpdatedMessage; + /** Track updates to a specific project */ + projectUpdated: ProjectUpdatedMessage; + /** Subscribe to when a project's versions get their preview image fully generated. */ + projectVersionsPreviewGenerated: ProjectVersionsPreviewGeneratedMessage; + /** Subscribe to changes to a project's versions. */ + projectVersionsUpdated: ProjectVersionsUpdatedMessage; + /** Subscribes to stream deleted event. Use this in clients/components that pertain only to this stream. */ + streamDeleted?: Maybe; + /** Subscribes to stream updated event. Use this in clients/components that pertain only to this stream. */ + streamUpdated?: Maybe; + /** Track newly added or deleted projects owned by the active user */ + userProjectsUpdated: UserProjectsUpdatedMessage; + /** + * Subscribes to new stream added event for your profile. Use this to display an up-to-date list of streams. + * **NOTE**: If someone shares a stream with you, this subscription will be triggered with an extra value of `sharedBy` in the payload. + */ + userStreamAdded?: Maybe; + /** + * Subscribes to stream removed event for your profile. Use this to display an up-to-date list of streams for your profile. + * **NOTE**: If someone revokes your permissions on a stream, this subscription will be triggered with an extra value of `revokedBy` in the payload. + */ + userStreamRemoved?: Maybe; + /** + * Broadcasts "real-time" location data for viewer users. + * @deprecated Use viewerUserActivityBroadcasted + */ + userViewerActivity?: Maybe; + /** Track user activities in the viewer relating to the specified resources */ + viewerUserActivityBroadcasted: ViewerUserActivityMessage; +}; + + +export type SubscriptionBranchCreatedArgs = { + streamId: Scalars['String']; +}; + + +export type SubscriptionBranchDeletedArgs = { + streamId: Scalars['String']; +}; + + +export type SubscriptionBranchUpdatedArgs = { + branchId?: InputMaybe; + streamId: Scalars['String']; +}; + + +export type SubscriptionCommentActivityArgs = { + resourceIds?: InputMaybe>>; + streamId: Scalars['String']; +}; + + +export type SubscriptionCommentThreadActivityArgs = { + commentId: Scalars['String']; + streamId: Scalars['String']; +}; + + +export type SubscriptionCommitCreatedArgs = { + streamId: Scalars['String']; +}; + + +export type SubscriptionCommitDeletedArgs = { + streamId: Scalars['String']; +}; + + +export type SubscriptionCommitUpdatedArgs = { + commitId?: InputMaybe; + streamId: Scalars['String']; +}; + + +export type SubscriptionProjectCommentsUpdatedArgs = { + target: ViewerUpdateTrackingTarget; +}; + + +export type SubscriptionProjectModelsUpdatedArgs = { + id: Scalars['String']; + modelIds?: InputMaybe>; +}; + + +export type SubscriptionProjectPendingModelsUpdatedArgs = { + id: Scalars['String']; +}; + + +export type SubscriptionProjectPendingVersionsUpdatedArgs = { + id: Scalars['String']; +}; + + +export type SubscriptionProjectUpdatedArgs = { + id: Scalars['String']; +}; + + +export type SubscriptionProjectVersionsPreviewGeneratedArgs = { + id: Scalars['String']; +}; + + +export type SubscriptionProjectVersionsUpdatedArgs = { + id: Scalars['String']; +}; + + +export type SubscriptionStreamDeletedArgs = { + streamId?: InputMaybe; +}; + + +export type SubscriptionStreamUpdatedArgs = { + streamId?: InputMaybe; +}; + + +export type SubscriptionUserViewerActivityArgs = { + resourceId: Scalars['String']; + streamId: Scalars['String']; +}; + + +export type SubscriptionViewerUserActivityBroadcastedArgs = { + sessionId?: InputMaybe; + target: ViewerUpdateTrackingTarget; +}; + +export type TestItem = { + __typename?: 'TestItem'; + bar: Scalars['String']; + foo: Scalars['String']; +}; + +export type UpdateModelInput = { + id: Scalars['ID']; + name: Scalars['String']; + projectId: Scalars['ID']; +}; + +/** Only non-null values will be updated */ +export type UpdateVersionInput = { + message?: InputMaybe; + versionId: Scalars['String']; +}; + +/** + * Full user type, should only be used in the context of admin operations or + * when a user is reading/writing info about himself + */ +export type User = { + __typename?: 'User'; + /** All the recent activity from this user in chronological order */ + activity?: Maybe; + /** Returns a list of your personal api tokens. */ + apiTokens?: Maybe>>; + /** Returns the apps you have authorized. */ + authorizedApps?: Maybe>>; + avatar?: Maybe; + bio?: Maybe; + /** + * Get commits authored by the user. If requested for another user, then only commits + * from public streams will be returned. + */ + commits?: Maybe; + company?: Maybe; + /** Returns the apps you have created. */ + createdApps?: Maybe>; + createdAt?: Maybe; + /** + * E-mail can be null, if it's requested for a user other than the authenticated one + * and the user isn't an admin + */ + email?: Maybe; + /** + * All the streams that a active user has favorited. + * Note: You can't use this to retrieve another user's favorite streams. + */ + favoriteStreams: StreamCollection; + /** Whether the user has a pending/active email verification token */ + hasPendingVerification?: Maybe; + id: Scalars['ID']; + /** Whether post-sign up onboarding has been finished or skipped entirely */ + isOnboardingFinished?: Maybe; + name: Scalars['String']; + notificationPreferences: Scalars['JSONObject']; + profiles?: Maybe; + /** Get all invitations to projects that the active user has */ + projectInvites: Array; + /** Get projects that the user participates in */ + projects: ProjectCollection; + role?: Maybe; + /** + * Returns all streams that the user is a collaborator on. If requested for a user, who isn't the + * authenticated user, then this will only return discoverable streams. + */ + streams: StreamCollection; + /** The user's timeline in chronological order */ + timeline?: Maybe; + /** Total amount of favorites attached to streams owned by the user */ + totalOwnedStreamsFavorites: Scalars['Int']; + verified?: Maybe; +}; + + +/** + * Full user type, should only be used in the context of admin operations or + * when a user is reading/writing info about himself + */ +export type UserActivityArgs = { + actionType?: InputMaybe; + after?: InputMaybe; + before?: InputMaybe; + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +/** + * Full user type, should only be used in the context of admin operations or + * when a user is reading/writing info about himself + */ +export type UserCommitsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +/** + * Full user type, should only be used in the context of admin operations or + * when a user is reading/writing info about himself + */ +export type UserFavoriteStreamsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +/** + * Full user type, should only be used in the context of admin operations or + * when a user is reading/writing info about himself + */ +export type UserProjectsArgs = { + cursor?: InputMaybe; + filter?: InputMaybe; + limit?: Scalars['Int']; +}; + + +/** + * Full user type, should only be used in the context of admin operations or + * when a user is reading/writing info about himself + */ +export type UserStreamsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + + +/** + * Full user type, should only be used in the context of admin operations or + * when a user is reading/writing info about himself + */ +export type UserTimelineArgs = { + after?: InputMaybe; + before?: InputMaybe; + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + +export type UserDeleteInput = { + email: Scalars['String']; +}; + +export type UserProjectsFilter = { + /** Only include projects where user has the specified roles */ + onlyWithRoles?: InputMaybe>; + /** Filter out projects by name */ + search?: InputMaybe; +}; + +export type UserProjectsUpdatedMessage = { + __typename?: 'UserProjectsUpdatedMessage'; + /** Project ID */ + id: Scalars['String']; + /** Project entity, null if project was deleted */ + project?: Maybe; + /** Message type */ + type: UserProjectsUpdatedMessageType; +}; + +export enum UserProjectsUpdatedMessageType { + Added = 'ADDED', + Removed = 'REMOVED' +} + +export type UserRoleInput = { + id: Scalars['String']; + role: Scalars['String']; +}; + +export type UserSearchResultCollection = { + __typename?: 'UserSearchResultCollection'; + cursor?: Maybe; + items: Array; +}; + +export type UserUpdateInput = { + avatar?: InputMaybe; + bio?: InputMaybe; + company?: InputMaybe; + name?: InputMaybe; +}; + +export type Version = { + __typename?: 'Version'; + authorUser?: Maybe; + /** All comment threads in this version */ + commentThreads: CommentCollection; + createdAt: Scalars['DateTime']; + id: Scalars['ID']; + message?: Maybe; + model: Model; + previewUrl: Scalars['String']; + referencedObject: Scalars['String']; + sourceApplication?: Maybe; +}; + + +export type VersionCommentThreadsArgs = { + cursor?: InputMaybe; + limit?: Scalars['Int']; +}; + +export type VersionCollection = { + __typename?: 'VersionCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']; +}; + +export type VersionMutations = { + __typename?: 'VersionMutations'; + delete: Scalars['Boolean']; + moveToModel: Model; + update: Version; +}; + + +export type VersionMutationsDeleteArgs = { + input: DeleteVersionsInput; +}; + + +export type VersionMutationsMoveToModelArgs = { + input: MoveVersionsInput; +}; + + +export type VersionMutationsUpdateArgs = { + input: UpdateVersionInput; +}; + +export type ViewerResourceGroup = { + __typename?: 'ViewerResourceGroup'; + /** Resource identifier used to refer to a collection of resource items */ + identifier: Scalars['String']; + /** Viewer resources that the identifier refers to */ + items: Array; +}; + +export type ViewerResourceItem = { + __typename?: 'ViewerResourceItem'; + /** Null if resource represents an object */ + modelId?: Maybe; + objectId: Scalars['String']; + /** Null if resource represents an object */ + versionId?: Maybe; +}; + +export type ViewerUpdateTrackingTarget = { + /** + * By default if resourceIdString is set, the "versionId" part of model resource identifiers will be ignored + * and all updates to of all versions of any of the referenced models will be returned. If `loadedVersionsOnly` is + * enabled, then only updates of loaded/referenced versions in resourceIdString will be returned. + */ + loadedVersionsOnly?: InputMaybe; + projectId: Scalars['String']; + /** + * Only request updates to the resources identified by this + * comma-delimited resouce string (same format that's used in the viewer URL) + */ + resourceIdString: Scalars['String']; +}; + +export type ViewerUserActivityMessage = { + __typename?: 'ViewerUserActivityMessage'; + sessionId: Scalars['String']; + /** SerializedViewerState, only null if DISCONNECTED */ + state?: Maybe; + status: ViewerUserActivityStatus; + user?: Maybe; + userId?: Maybe; + userName: Scalars['String']; +}; + +export type ViewerUserActivityMessageInput = { + sessionId: Scalars['String']; + /** SerializedViewerState, only null if DISCONNECTED */ + state?: InputMaybe; + status: ViewerUserActivityStatus; + userId?: InputMaybe; + userName: Scalars['String']; +}; + +export enum ViewerUserActivityStatus { + Disconnected = 'DISCONNECTED', + Viewing = 'VIEWING' +} + +export type Webhook = { + __typename?: 'Webhook'; + description?: Maybe; + enabled?: Maybe; + history?: Maybe; + id: Scalars['String']; + streamId: Scalars['String']; + triggers: Array>; + url: Scalars['String']; +}; + + +export type WebhookHistoryArgs = { + limit?: Scalars['Int']; +}; + +export type WebhookCollection = { + __typename?: 'WebhookCollection'; + items?: Maybe>>; + totalCount?: Maybe; +}; + +export type WebhookCreateInput = { + description?: InputMaybe; + enabled?: InputMaybe; + secret?: InputMaybe; + streamId: Scalars['String']; + triggers: Array>; + url: Scalars['String']; +}; + +export type WebhookDeleteInput = { + id: Scalars['String']; + streamId: Scalars['String']; +}; + +export type WebhookEvent = { + __typename?: 'WebhookEvent'; + id: Scalars['String']; + lastUpdate: Scalars['DateTime']; + payload: Scalars['String']; + retryCount: Scalars['Int']; + status: Scalars['Int']; + statusInfo: Scalars['String']; + webhookId: Scalars['String']; +}; + +export type WebhookEventCollection = { + __typename?: 'WebhookEventCollection'; + items?: Maybe>>; + totalCount?: Maybe; +}; + +export type WebhookUpdateInput = { + description?: InputMaybe; + enabled?: InputMaybe; + id: Scalars['String']; + secret?: InputMaybe; + streamId: Scalars['String']; + triggers?: InputMaybe>>; + url?: InputMaybe; +}; + +export type CrossSyncCommitBranchMetadataQueryVariables = Exact<{ + streamId: Scalars['String']; + commitId: Scalars['String']; +}>; + + +export type CrossSyncCommitBranchMetadataQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', commit?: { __typename?: 'Commit', id: string, branchName?: string | null } | null } | null }; + +export type CrossSyncBranchMetadataQueryVariables = Exact<{ + streamId: Scalars['String']; + branchName: Scalars['String']; +}>; + + +export type CrossSyncBranchMetadataQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', branch?: { __typename?: 'Branch', id: string } | null } | null }; + +export type CrossSyncCommitDownloadMetadataQueryVariables = Exact<{ + streamId: Scalars['String']; + commitId: Scalars['String']; +}>; + + +export type CrossSyncCommitDownloadMetadataQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', commit?: { __typename?: 'Commit', id: string, referencedObject: string, authorId?: string | null, message?: string | null, createdAt?: string | null, sourceApplication?: string | null, totalChildrenCount?: number | null, parents?: Array | null } | null } | null }; + +export type CrossSyncProjectViewerResourcesQueryVariables = Exact<{ + projectId: Scalars['String']; + resourceUrlString: Scalars['String']; +}>; + + +export type CrossSyncProjectViewerResourcesQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, viewerResources: Array<{ __typename?: 'ViewerResourceGroup', identifier: string, items: Array<{ __typename?: 'ViewerResourceItem', modelId?: string | null, versionId?: string | null, objectId: string }> }> } }; + +export type CrossSyncDownloadableCommitViewerThreadsQueryVariables = Exact<{ + projectId: Scalars['String']; + filter: ProjectCommentsFilter; + cursor?: InputMaybe; + limit?: InputMaybe; +}>; + + +export type CrossSyncDownloadableCommitViewerThreadsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, commentThreads: { __typename?: 'ProjectCommentCollection', totalCount: number, totalArchivedCount: number, items: Array<{ __typename?: 'Comment', id: string, viewerState?: Record | null, screenshot?: string | null, replies: { __typename?: 'CommentCollection', items: Array<{ __typename?: 'Comment', id: string, viewerState?: Record | null, screenshot?: string | null, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null } }> }, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null } }> } } }; + +export type DownloadbleCommentMetadataFragment = { __typename?: 'Comment', id: string, viewerState?: Record | null, screenshot?: string | null, text: { __typename?: 'SmartTextEditorValue', doc?: Record | null } }; + +export type CrossSyncProjectMetadataQueryVariables = Exact<{ + id: Scalars['String']; + versionsCursor?: InputMaybe; +}>; + + +export type CrossSyncProjectMetadataQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, name: string, description?: string | null, visibility: ProjectVisibility, versions: { __typename?: 'VersionCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'Version', id: string, createdAt: string, model: { __typename?: 'Model', id: string, name: string } }> } } }; + +export type CrossSyncClientTestQueryVariables = Exact<{ [key: string]: never; }>; + + +export type CrossSyncClientTestQuery = { __typename?: 'Query', _?: string | null }; diff --git a/packages/server/modules/cross-server-sync/index.ts b/packages/server/modules/cross-server-sync/index.ts new file mode 100644 index 000000000..4108fa818 --- /dev/null +++ b/packages/server/modules/cross-server-sync/index.ts @@ -0,0 +1,17 @@ +import { moduleLogger, crossServerSyncLogger } from '@/logging/logging' +import { ensureOnboardingProject } from '@/modules/cross-server-sync/services/onboardingProject' +import { SpeckleModule } from '@/modules/shared/helpers/typeHelper' + +const crossServerSyncModule: SpeckleModule = { + init() { + moduleLogger.info('🔄️ Init cross-server-sync module') + }, + finalize() { + crossServerSyncLogger.info('⬇️ Ensuring base onboarding stream asynchronously...') + void ensureOnboardingProject().catch((err) => + crossServerSyncLogger.error(err, 'Error ensuring onboarding stream') + ) + } +} + +export = crossServerSyncModule diff --git a/packages/server/modules/cli/services/download/commit.ts b/packages/server/modules/cross-server-sync/services/commit.ts similarity index 69% rename from packages/server/modules/cli/services/download/commit.ts rename to packages/server/modules/cross-server-sync/services/commit.ts index 44620c772..69e65a432 100644 --- a/packages/server/modules/cli/services/download/commit.ts +++ b/packages/server/modules/cross-server-sync/services/commit.ts @@ -1,20 +1,7 @@ import fetch from 'cross-fetch' -import { - ApolloClient, - InMemoryCache, - NormalizedCacheObject, - gql, - HttpLink, - ApolloQueryResult -} from '@apollo/client/core' -import { setContext } from '@apollo/client/link/context' -import { getFrontendOrigin, getServerVersion } from '@/modules/shared/helpers/envHelper' -import { - Commit, - ViewerResourceGroup, - Comment, - CreateCommentInput -} from '@/test/graphql/generated/graphql' +import { ApolloClient, NormalizedCacheObject, gql } from '@apollo/client/core' +import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper' +import { CreateCommentInput } from '@/test/graphql/generated/graphql' import { getStreamBranchByName } from '@/modules/core/repositories/branches' import { getStream, getStreamCollaborators } from '@/modules/core/repositories/streams' import { Roles } from '@speckle/shared' @@ -23,7 +10,7 @@ import { createObject } from '@/modules/core/services/objects' import { getObject } from '@/modules/core/repositories/objects' import ObjectLoader from '@speckle/objectloader' import { noop } from 'lodash' -import { cliLogger } from '@/logging/logging' +import { Logger, crossServerSyncLogger } from '@/logging/logging' import { createCommitByBranchId } from '@/modules/core/services/commit/management' import { getUser } from '@/modules/core/repositories/users' import type { SpeckleViewer } from '@speckle/shared' @@ -31,6 +18,18 @@ import { createCommentThreadAndNotify, createCommentReplyAndNotify } from '@/modules/comments/services/management' +import { + createApolloClient, + assertValidGraphQLResult +} from '@/modules/cross-server-sync/utils/graphqlClient' +import { CrossServerCommitSyncError } from '@/modules/cross-server-sync/errors' +import { + CrossSyncBranchMetadataQuery, + CrossSyncCommitBranchMetadataQuery, + CrossSyncCommitDownloadMetadataQuery, + CrossSyncDownloadableCommitViewerThreadsQuery, + CrossSyncProjectViewerResourcesQuery +} from '@/modules/cross-server-sync/graph/generated/graphql' type LocalResources = Awaited> type LocalResourcesWithCommit = LocalResources & { newCommitId: string } @@ -42,17 +41,14 @@ type ObjectLoaderObject = Record & { totalChildrenCount: number } +type CommitMetadata = Awaited> +type ViewerThread = Awaited>[0] + const COMMIT_URL_RGX = /((https?:\/\/)?[\w.]+)\/streams\/([\w]+)\/commits\/([\w]+)/i const MODEL_URL_RGX = /((https?:\/\/)?[\w.]+)\/projects\/([\w]+)\/models\/([\w@,]+)/i -const testQuery = gql` - query CommitDownloadTest { - _ - } -` - const commitBranchMetadataQuery = gql` - query CommitBranchMetadata($streamId: String!, $commitId: String!) { + query CrossSyncCommitBranchMetadata($streamId: String!, $commitId: String!) { stream(id: $streamId) { commit(id: $commitId) { id @@ -63,7 +59,7 @@ const commitBranchMetadataQuery = gql` ` const branchMetadataQuery = gql` - query BranchMetadata($streamId: String!, $branchName: String!) { + query CrossSyncBranchMetadata($streamId: String!, $branchName: String!) { stream(id: $streamId) { branch(name: $branchName) { id @@ -73,7 +69,7 @@ const branchMetadataQuery = gql` ` const commitMetadataQuery = gql` - query CommitDownloadMetadata($streamId: String!, $commitId: String!) { + query CrossSyncCommitDownloadMetadata($streamId: String!, $commitId: String!) { stream(id: $streamId) { commit(id: $commitId) { id @@ -90,7 +86,10 @@ const commitMetadataQuery = gql` ` const viewerResourcesQuery = gql` - query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) { + query CrossSyncProjectViewerResources( + $projectId: String! + $resourceUrlString: String! + ) { project(id: $projectId) { id viewerResources(resourceIdString: $resourceUrlString) { @@ -106,7 +105,7 @@ const viewerResourcesQuery = gql` ` const viewerThreadsQuery = gql` - query DownloadableCommitViewerThreads( + query CrossSyncDownloadableCommitViewerThreads( $projectId: String! $filter: ProjectCommentsFilter! $cursor: String @@ -139,64 +138,6 @@ const viewerThreadsQuery = gql` } ` -const assertValidGraphQLResult = ( - res: ApolloQueryResult, - operationName: string -) => { - if (res.errors?.length) { - throw new Error( - `GQL operation '${operationName}' failed because of errors: ` + - JSON.stringify(res.errors) - ) - } -} - -const createApolloClient = async ( - origin: string, - params?: { token?: string } -): Promise => { - const cache = new InMemoryCache() - - const baseLink = new HttpLink({ uri: `${origin}/graphql`, fetch }) - const authLink = setContext((_, { headers }) => { - return { - headers: { - ...headers, - authorization: params?.token ? `Bearer ${params.token}` : '' - } - } - }) - - const client = new ApolloClient({ - link: authLink.concat(baseLink), - cache, - name: 'cli', - version: getServerVersion(), - defaultOptions: { - query: { - fetchPolicy: 'no-cache', - errorPolicy: 'all' - } - } - }) - - // Test it out - const res = await client.query({ - query: testQuery - }) - - assertValidGraphQLResult(res, 'Target server test query') - - if (!res.data?._) { - throw new Error( - "Couldn't construct working Apollo Client, test query failed cause of unexpected response: " + - JSON.stringify(res.data) - ) - } - - return client -} - const parseCommitUrl = async (url: string, token?: string) => { const [, origin, , streamId, commitId] = COMMIT_URL_RGX.exec(url) || [] if (!origin || !streamId || !commitId) { @@ -253,7 +194,7 @@ const parseIncomingUrl = async (url: string, token?: string) => { return modelUrl } - throw new Error(`Couldn't parse commit URL: ${url}`) + throw new CrossServerCommitSyncError(`Couldn't parse commit URL: ${url}`) } const getLocalResources = async ( @@ -263,12 +204,14 @@ const getLocalResources = async ( ) => { const targetStream = await getStream({ streamId: targetStreamId }) if (!targetStream) { - throw new Error(`Couldn't find local stream with id ${targetStreamId}`) + throw new CrossServerCommitSyncError( + `Couldn't find local stream with id ${targetStreamId}` + ) } const targetBranch = await getStreamBranchByName(targetStreamId, branchName) if (!targetBranch) { - throw new Error( + throw new CrossServerCommitSyncError( `Couldn't find local branch ${branchName} in stream ${targetStreamId}` ) } @@ -285,7 +228,7 @@ const getViewerResources = async ( client: GraphQLClient, params: { projectId: string; resourceUrlString: string } ) => { - const results = await client.query({ + const results = await client.query({ query: viewerResourcesQuery, variables: params }) @@ -293,10 +236,12 @@ const getViewerResources = async ( const viewerResources = results.data?.project?.viewerResources if (!viewerResources) { - throw new Error('Unexpectedly received invalid viewer resources structure') + throw new CrossServerCommitSyncError( + 'Unexpectedly received invalid viewer resources structure' + ) } - return viewerResources as ViewerResourceGroup[] + return viewerResources } const getCommitBranchId = async ( @@ -304,18 +249,19 @@ const getCommitBranchId = async ( params: { streamId: string; commitId: string } ) => { const { streamId, commitId } = params - const commitBranchMetadataRes = await client.query({ - query: commitBranchMetadataQuery, - variables: { streamId, commitId } - }) + const commitBranchMetadataRes = + await client.query({ + query: commitBranchMetadataQuery, + variables: { streamId, commitId } + }) assertValidGraphQLResult(commitBranchMetadataRes, 'Commit Branch Metadata Query') const branchName = commitBranchMetadataRes.data?.stream?.commit?.branchName if (!branchName) { - throw new Error('Could not resolve commit branch name') + throw new CrossServerCommitSyncError('Could not resolve commit branch name') } - const branchMetadataRes = await client.query({ + const branchMetadataRes = await client.query({ query: branchMetadataQuery, variables: { streamId, branchName } }) @@ -323,15 +269,15 @@ const getCommitBranchId = async ( const branchId = branchMetadataRes.data?.stream?.branch?.id if (!branchId) { - throw new Error('Could not resolve commit branch id') + throw new CrossServerCommitSyncError('Could not resolve commit branch id') } - return branchId as string + return branchId } const getCommitMetadata = async (client: GraphQLClient, params: ParsedCommitUrl) => { const { streamId, commitId } = params - const results = await client.query({ + const results = await client.query({ query: commitMetadataQuery, variables: { streamId, commitId } }) @@ -339,16 +285,18 @@ const getCommitMetadata = async (client: GraphQLClient, params: ParsedCommitUrl) const commit = results.data?.stream?.commit if (!commit) { - throw new Error('Unexpectedly received invalid commit structure') + throw new CrossServerCommitSyncError( + 'Unexpectedly received invalid commit structure' + ) } - return commit as Commit + return commit } const getViewerThreads = async (client: GraphQLClient, params: ParsedCommitUrl) => { const { streamId, branchId, commitId } = params - const results = await client.query({ + const results = await client.query({ query: viewerThreadsQuery, variables: { projectId: streamId, @@ -364,10 +312,12 @@ const getViewerThreads = async (client: GraphQLClient, params: ParsedCommitUrl) const threads = results.data?.project?.commentThreads?.items if (!threads) { - throw new Error('Unexpectedly received invalid viewer threads structure') + throw new CrossServerCommitSyncError( + 'Unexpectedly received invalid viewer threads structure' + ) } - return threads as Comment[] + return threads } const cleanViewerState = ( @@ -393,33 +343,40 @@ const cleanViewerState = ( }) const saveNewThreads = async ( - threads: Comment[], - localResources: LocalResourcesWithCommit + threads: ViewerThread[], + localResources: LocalResourcesWithCommit, + options?: Partial<{ + logger: typeof crossServerSyncLogger + }> ) => { + const { logger = crossServerSyncLogger } = options || {} const { commentAuthor, targetStream } = localResources if (!commentAuthor) return - const threadInputs: { originalComment: Comment; input: CreateCommentInput }[] = - threads.map((t) => ({ - originalComment: t, - input: { - projectId: targetStream.id, - content: { - doc: t.text.doc, - blobIds: [] - }, - viewerState: t.viewerState - ? cleanViewerState( - t.viewerState as SpeckleViewer.ViewerState.SerializedViewerState, - localResources - ) - : null, - screenshot: t.screenshot, - resourceIdString: `${localResources.targetBranch.id}@${localResources.newCommitId}` - } - })) + const threadInputs: { originalComment: ViewerThread; input: CreateCommentInput }[] = + threads + .filter((t) => !!t.text.doc) + .map((t) => ({ + originalComment: t, + input: { + projectId: targetStream.id, + content: { + doc: t.text.doc, + blobIds: [] // TODO: Currently not supported + }, + viewerState: t.viewerState + ? cleanViewerState( + t.viewerState as SpeckleViewer.ViewerState.SerializedViewerState, + localResources + ) + : null, + screenshot: t.screenshot, + resourceIdString: `${localResources.targetBranch.id}@${localResources.newCommitId}` + } + })) + if (!threadInputs.length) return - cliLogger.info(`Creating ${threadInputs.length} new comment threads...`) + logger.info(`Creating ${threadInputs.length} new comment threads...`) const res = await Promise.all( threadInputs.map((i) => createCommentThreadAndNotify(i.input, commentAuthor.id).then((c) => ({ @@ -428,7 +385,7 @@ const saveNewThreads = async ( })) ) ) - cliLogger.info(`...created ${res.length} new comment threads!`) + logger.info(`...created ${res.length} new comment threads!`) for (const resItem of res) { const { originalData, newComment } = resItem @@ -436,7 +393,7 @@ const saveNewThreads = async ( const { replies } = originalComment if (!replies) continue - cliLogger.info( + logger.info( `Creating ${replies.items.length} new replies for comment thread ${originalComment.id}...` ) await Promise.all( @@ -455,11 +412,14 @@ const saveNewThreads = async ( ) ) ) - cliLogger.info(`...created ${replies.items.length} new replies!`) + logger.info(`...created ${replies.items.length} new replies!`) } } -const saveNewCommit = async (commit: Commit, localResources: LocalResources) => { +const saveNewCommit = async ( + commit: CommitMetadata, + localResources: LocalResources +) => { const { targetStream, targetBranch, owner } = localResources const streamId = targetStream.id @@ -504,10 +464,14 @@ const saveNewCommit = async (commit: Commit, localResources: LocalResources) => const createNewObject = async ( newObject: ObjectLoaderObject, - targetStreamId: string + targetStreamId: string, + options?: Partial<{ + logger: typeof crossServerSyncLogger + }> ) => { + const { logger = crossServerSyncLogger } = options || {} if (!newObject) { - cliLogger.error('Encountered falsy object!') + logger.error('Encountered falsy object!') return } @@ -519,17 +483,25 @@ const createNewObject = async ( const newRecord = await getObject(newObjectId, targetStreamId) if (!newRecord) { - throw new Error("Unexpected error! Just inserted an object, but can't find it!") + throw new CrossServerCommitSyncError( + "Unexpected error! Just inserted an object, but can't find it!" + ) } return newRecord } -const loadAllObjectsFromParent = async (params: { - targetStreamId: string - sourceCommit: Commit - parsedCommitUrl: ParsedCommitUrl -}) => { +const loadAllObjectsFromParent = async ( + params: { + targetStreamId: string + sourceCommit: CommitMetadata + parsedCommitUrl: ParsedCommitUrl + }, + options?: Partial<{ + logger: typeof crossServerSyncLogger + }> +) => { + const { logger = crossServerSyncLogger } = options || {} const { targetStreamId, sourceCommit, @@ -549,29 +521,51 @@ const loadAllObjectsFromParent = async (params: { let processedObjectCount = 1 for await (const obj of objectLoader.getObjectIterator()) { const typedObj = obj as ObjectLoaderObject - cliLogger.info( - `Processing ${obj.id} - ${processedObjectCount++}/${totalObjectCount}` - ) - await createNewObject(typedObj, targetStreamId) + logger.debug(`Processing ${obj.id} - ${processedObjectCount++}/${totalObjectCount}`) + await createNewObject(typedObj, targetStreamId, { logger }) } } +/** + * Downloads a commit/version (both FE1 and FE2 supported) from an external Speckle server instance + */ export const downloadCommit = async ( argv: { + /** + * A FE1 commit URL or an FE2 model/version URL + */ commitUrl: string + /** + * ID of the local stream that should receive the commit + */ targetStreamId: string - branchName: string + /** + * Stream branch that should receive the commit. Defaults to 'main' + */ + branchName?: string + /** + * Specify if target commit is private + */ token?: string + /** + * Specify if you want comments to be pulled in also + */ commentAuthorId?: string }, options?: Partial<{ - logger: typeof cliLogger + logger: Logger }> ) => { - const { commitUrl, targetStreamId, branchName, token, commentAuthorId } = argv - const { logger = cliLogger } = options || {} + const { + commitUrl, + targetStreamId, + branchName = 'main', + token, + commentAuthorId + } = argv + const { logger = crossServerSyncLogger } = options || {} - logger.debug(`Process started at: ${new Date().toISOString()}`) + logger.debug(`Commit/version download started at: ${new Date().toISOString()}`) const localResources = await getLocalResources( targetStreamId, @@ -598,16 +592,19 @@ export const downloadCommit = async ( logger.debug(`Created new local commit: ${newCommitId}`) logger.debug(`Pulling & saving all objects! (${commit.totalChildrenCount})`) - await loadAllObjectsFromParent({ - targetStreamId, - sourceCommit: commit, - parsedCommitUrl - }) + await loadAllObjectsFromParent( + { + targetStreamId, + sourceCommit: commit, + parsedCommitUrl + }, + { logger } + ) if (localResources.commentAuthor) { logger.debug(`Pulling & saving all comments w/ #${commentAuthorId} as author!`) const threads = await getViewerThreads(client, parsedCommitUrl) - await saveNewThreads(threads, newResources) + await saveNewThreads(threads, newResources, { logger }) } const linkToNewCommit = parsedCommitUrl.isFe2 diff --git a/packages/server/modules/cross-server-sync/services/onboardingProject.ts b/packages/server/modules/cross-server-sync/services/onboardingProject.ts new file mode 100644 index 000000000..a423e25c5 --- /dev/null +++ b/packages/server/modules/cross-server-sync/services/onboardingProject.ts @@ -0,0 +1,70 @@ +import { crossServerSyncLogger } from '@/logging/logging' +import { + getOnboardingBaseStream, + markOnboardingBaseStream +} from '@/modules/core/repositories/streams' +import { getFirstAdmin } from '@/modules/core/repositories/users' +import { downloadProject } from '@/modules/cross-server-sync/services/project' +import { + getOnboardingStreamCacheBustNumber, + getOnboardingStreamUrl +} from '@/modules/shared/helpers/envHelper' + +const getMetadata = () => { + const url = getOnboardingStreamUrl() + const cacheBustNumber = getOnboardingStreamCacheBustNumber() + if (!url) return null + + const version = `${url}:::${cacheBustNumber}` + return { url, cacheBustNumber, version } +} + +export async function getOnboardingBaseProject() { + const metadata = getMetadata() + if (!metadata) { + return undefined + } + + return await getOnboardingBaseStream(metadata.version) +} + +export async function ensureOnboardingProject() { + const logger = crossServerSyncLogger + logger.info('Ensuring onboarding project is present...') + + const metadata = getMetadata() + if (!metadata) { + logger.info('No base onboarding stream configured through env vars...') + return undefined + } + + const [existingStream, admin] = await Promise.all([ + getOnboardingBaseStream(metadata.version), + getFirstAdmin() + ]) + if (existingStream) { + logger.debug('Onboarding stream already exists, skipping...') + return existingStream + } + if (!admin) { + logger.info('No admin user found, skipping onboarding stream creation...') + return undefined + } + + logger.debug('Onboarding stream not found, pulling from target server...') + const res = await downloadProject( + { + projectUrl: metadata.url, + authorId: admin.id, + syncComments: true + }, + { logger } + ) + + logger.debug('Marking stream as onboarding base...') + await markOnboardingBaseStream(res.projectId, metadata.version) + + logger.info('Onboarding base stream created successfully!') + + return res.project +} diff --git a/packages/server/modules/cross-server-sync/services/project.ts b/packages/server/modules/cross-server-sync/services/project.ts new file mode 100644 index 000000000..34ee349b9 --- /dev/null +++ b/packages/server/modules/cross-server-sync/services/project.ts @@ -0,0 +1,227 @@ +import { crossServerSyncLogger, Logger } from '@/logging/logging' +import { getUser } from '@/modules/core/repositories/users' +import { CrossServerProjectSyncError } from '@/modules/cross-server-sync/errors' +import { + createApolloClient, + GraphQLClient, + gql, + assertValidGraphQLResult +} from '@/modules/cross-server-sync/utils/graphqlClient' +import { CrossSyncProjectMetadataQuery } from '@/modules/cross-server-sync/graph/generated/graphql' +import { omit } from 'lodash' +import { downloadCommit } from '@/modules/cross-server-sync/services/commit' +import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper' +import { createStreamReturnRecord } from '@/modules/core/services/streams/management' +import { createBranchAndNotify } from '@/modules/core/services/branch/management' +import { getStreamBranchByName } from '@/modules/core/repositories/branches' + +type ProjectMetadata = Awaited> + +const PROJECT_URL_RGX = /((https?:\/\/)?[\w.]+)\/projects\/([\w]+)\/?/i + +const projectMetadataQuery = gql` + query CrossSyncProjectMetadata($id: String!, $versionsCursor: String) { + project(id: $id) { + id + name + description + visibility + versions(limit: 100, cursor: $versionsCursor) { + totalCount + cursor + items { + id + createdAt + model { + id + name + } + } + } + } + } +` + +const getLocalResources = async (params: { authorId: string }) => { + const { authorId } = params + const user = await getUser(authorId) + if (!user) { + throw new CrossServerProjectSyncError('Target author not found') + } + + return { user } +} + +const parseIncomingUrl = (projectUrl: string) => { + const [, origin, , projectId] = PROJECT_URL_RGX.exec(projectUrl) || [] + if (!origin || !projectId) { + throw new CrossServerProjectSyncError('Invalid project URL') + } + + return { origin, projectId } +} + +const getProjectMetadata = async (params: { + client: GraphQLClient + projectId: string +}) => { + const { client, projectId } = params + + // Load 1st page + const res = await client.query({ + query: projectMetadataQuery, + variables: { + id: projectId + } + }) + assertValidGraphQLResult(res, 'Project metadata query') + + const projectInfo = omit(res.data.project, ['versions']) + const versions = res.data.project.versions.items + + // Load all pages of versions + let cursor = res.data.project.versions.cursor + let failsafe = 10 + while (cursor && failsafe-- > 0) { + const res = await client.query({ + query: projectMetadataQuery, + variables: { + id: projectId, + versionsCursor: cursor + } + }) + assertValidGraphQLResult(res, 'Project metadata query') + versions.push(...res.data.project.versions.items) + cursor = res.data.project.versions.cursor + } + + // Sort versions by descending creation data + versions.sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ) + + return { projectInfo, versions } +} + +const ensureBranch = async (params: { + streamId: string + branchName: string + authorId: string +}) => { + const { streamId, branchName, authorId } = params + const existingBranch = await getStreamBranchByName(streamId, branchName) + if (!existingBranch) { + const newBranch = await createBranchAndNotify( + { + streamId, + name: branchName + }, + authorId + ) + return newBranch + } + + return existingBranch +} + +const importVersions = async (params: { + logger: Logger + projectInfo: ProjectMetadata + localProjectId: string + localAuthorId: string + origin: string + syncComments?: boolean +}) => { + const { logger, projectInfo, origin, localProjectId, syncComments, localAuthorId } = + params + const projectId = projectInfo.projectInfo.id + + logger.debug(`Serially downloading ${projectInfo.versions.length} versions...`) + for (const version of projectInfo.versions) { + // Ensure branch exists + const branchName = version.model.name + await ensureBranch({ + streamId: localProjectId, + branchName, + authorId: localAuthorId + }) + + // Actually download + const url = new URL( + `/projects/${projectId}/models/${version.model.id}@${version.id}`, + origin + ) + await downloadCommit( + { + commitUrl: url.toString(), + targetStreamId: localProjectId, + commentAuthorId: syncComments ? localAuthorId : undefined, + branchName + }, + { logger } + ) + } +} + +/** + * Downloads a project from an external FE2 Speckle server instance + */ +export const downloadProject = async ( + params: { + /** + * An FE2 project URL (must be publicly accessible) + */ + projectUrl: string + /** + * ID of user that should own the project locally + */ + authorId: string + syncComments?: boolean + }, + options?: Partial<{ + logger: Logger + }> +) => { + const { projectUrl, authorId, syncComments } = params + const { logger = crossServerSyncLogger } = options || {} + + logger.info(`Project download started at: ${new Date().toISOString()}`) + + const localResources = await getLocalResources({ authorId }) + const parsedUrl = parseIncomingUrl(projectUrl) + const client = await createApolloClient(parsedUrl.origin) + + logger.debug(`Resolving project metadata and associated versions...`) + const projectInfo = await getProjectMetadata({ + client, + projectId: parsedUrl.projectId + }) + + logger.debug(`Creating project locally...`) + const project = await createStreamReturnRecord({ + ...projectInfo.projectInfo, + ownerId: localResources.user.id + }) + + await importVersions({ + logger, + projectInfo, + localProjectId: project.id, + localAuthorId: localResources.user.id, + origin: parsedUrl.origin, + syncComments + }) + logger.info(`Project download completed at: ${new Date().toISOString()}`) + + const newProjectUrl = new URL( + `/projects/${project.id}`, + getFrontendOrigin(true) + ).toString() + logger.info(`New Project URL: ${newProjectUrl}`) + + return { + newProjectUrl, + projectId: project.id, + project + } +} diff --git a/packages/server/modules/cross-server-sync/utils/graphqlClient.ts b/packages/server/modules/cross-server-sync/utils/graphqlClient.ts new file mode 100644 index 000000000..7d99e83aa --- /dev/null +++ b/packages/server/modules/cross-server-sync/utils/graphqlClient.ts @@ -0,0 +1,79 @@ +import { + ApolloClient, + InMemoryCache, + NormalizedCacheObject, + HttpLink, + gql, + ApolloQueryResult +} from '@apollo/client/core' +import { setContext } from '@apollo/client/link/context' +import { getServerVersion } from '@/modules/shared/helpers/envHelper' +import { CrossSyncClientTestQuery } from '@/modules/cross-server-sync/graph/generated/graphql' + +export type GraphQLClient = ApolloClient + +const testQuery = gql` + query CrossSyncClientTest { + _ + } +` + +export const assertValidGraphQLResult = ( + res: ApolloQueryResult, + operationName: string +) => { + if (res.errors?.length) { + throw new Error( + `GQL operation '${operationName}' failed because of errors: ` + + JSON.stringify(res.errors) + ) + } +} + +export const createApolloClient = async ( + origin: string, + params?: { token?: string } +): Promise => { + const cache = new InMemoryCache() + + const baseLink = new HttpLink({ uri: `${origin}/graphql`, fetch }) + const authLink = setContext((_, { headers }) => { + return { + headers: { + ...headers, + authorization: params?.token ? `Bearer ${params.token}` : '' + } + } + }) + + const client = new ApolloClient({ + link: authLink.concat(baseLink), + cache, + name: 'cli', + version: getServerVersion(), + defaultOptions: { + query: { + fetchPolicy: 'no-cache', + errorPolicy: 'all' + } + } + }) + + // Test it out + const res = await client.query({ + query: testQuery + }) + + assertValidGraphQLResult(res, 'Target server test query') + + if (!res.data?._) { + throw new Error( + "Couldn't construct working Apollo Client, test query failed cause of unexpected response: " + + JSON.stringify(res.data) + ) + } + + return client +} + +export { gql } diff --git a/packages/server/modules/index.js b/packages/server/modules/index.js index c8aff7281..0efa451de 100644 --- a/packages/server/modules/index.js +++ b/packages/server/modules/index.js @@ -56,7 +56,8 @@ async function getSpeckleModules() { './notifications', './activitystream', './accessrequests', - './webhooks' + './webhooks', + './cross-server-sync' ] for (const dir of moduleDirs) { @@ -72,7 +73,7 @@ exports.init = async (app) => { // Stage 1: initialise all modules for (const module of modules) { - await module.init(app, isInitial) + await module.init?.(app, isInitial) } // Stage 2: finalize init all modules diff --git a/packages/server/modules/shared/helpers/envHelper.ts b/packages/server/modules/shared/helpers/envHelper.ts index 3fc154e28..dfa449064 100644 --- a/packages/server/modules/shared/helpers/envHelper.ts +++ b/packages/server/modules/shared/helpers/envHelper.ts @@ -1,4 +1,3 @@ -import { Nullable } from '@speckle/shared' import { MisconfiguredEnvironmentError } from '@/modules/shared/errors' import { trimEnd } from 'lodash' @@ -159,13 +158,6 @@ export function isSSLServer() { return /^https:\/\//.test(getBaseUrl()) } -/** - * Source stream for cloning tutorial/guide streams for users - */ -export function getOnboardingStreamId(): Nullable { - return process.env.ONBOARDING_STREAM_ID || null -} - export function adminOverrideEnabled() { return process.env.ADMIN_OVERRIDE_ENABLED === 'true' } @@ -187,3 +179,28 @@ export function speckleAutomateUrl() { export function ignoreMissingMigrations() { return ['1', 'true'].includes(process.env.IGNORE_MISSING_MIRATIONS || 'false') } + +/** + * URL of a project on any FE2 speckle server that will be pulled in and used as the onboarding stream + */ +export function getOnboardingStreamUrl() { + const val = process.env.ONBOARDING_STREAM_URL + if (!val?.length) return null + + try { + // validating that the URL is valid + return new URL(val).toString() + } catch (e) { + // suppress + } + + return null +} + +/** + * Increase this value to re-sync the onboarding stream + */ +export function getOnboardingStreamCacheBustNumber() { + const val = process.env.ONBOARDING_STREAM_CACHE_BUST_NUMBER || '1' + return parseInt(val) || 1 +} diff --git a/packages/server/modules/shared/helpers/typeHelper.ts b/packages/server/modules/shared/helpers/typeHelper.ts index aa876cd19..842481c9b 100644 --- a/packages/server/modules/shared/helpers/typeHelper.ts +++ b/packages/server/modules/shared/helpers/typeHelper.ts @@ -24,7 +24,7 @@ export type SpeckleModule = Record MaybeAsync + init?: (app: Express, isInitial: boolean) => MaybeAsync /** * Finalize initialization. This is only invoked once all of the other modules' `init()` * hooks are run. From 5d3f1cda394bf56724a73b0798ae25f22875f417 Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Mon, 31 Jul 2023 11:58:22 +0300 Subject: [PATCH 06/11] fix: missing helm chart values for new onboarding stream sync (#1740) --- .../templates/frontend_2/deployment.yml | 4 ++++ utils/helm/speckle-server/values.schema.json | 15 +++++++++++++++ utils/helm/speckle-server/values.yaml | 7 +++++++ 3 files changed, 26 insertions(+) diff --git a/utils/helm/speckle-server/templates/frontend_2/deployment.yml b/utils/helm/speckle-server/templates/frontend_2/deployment.yml index cdcb56369..35ef7c515 100644 --- a/utils/helm/speckle-server/templates/frontend_2/deployment.yml +++ b/utils/helm/speckle-server/templates/frontend_2/deployment.yml @@ -66,6 +66,10 @@ spec: value: {{ .Values.frontend_2.logClientApiToken }} - name: NUXT_PUBLIC_LOG_CLIENT_API_ENDPOINT value: {{ .Values.frontend_2.logClientApiEndpoint }} + - name: ONBOARDING_STREAM_URL + value: {{ .Values.frontend_2.onboarding.stream_url }} + - name: ONBOARDING_STREAM_CACHE_BUST_NUMBER + value: {{ .Values.frontend_2.onboarding.stream_cache_bust_number }} priorityClassName: high-priority diff --git a/utils/helm/speckle-server/values.schema.json b/utils/helm/speckle-server/values.schema.json index 850a6e2ba..ba5fd5b19 100644 --- a/utils/helm/speckle-server/values.schema.json +++ b/utils/helm/speckle-server/values.schema.json @@ -1162,6 +1162,21 @@ "description": "The number of instances of the Frontend 2 server prod to be deployed withing the cluster.", "default": 1 }, + "onboarding": { + "type": "object", + "properties": { + "stream_url": { + "type": "string", + "description": "The (cross-server) URL to the project/stream that should be used as the onboarding project base.", + "default": "https://latest.speckle.systems/projects/843d07eb10" + }, + "stream_cache_bust_number": { + "type": "number", + "description": "Increase this number to trigger the re-pulling of the base stream", + "default": 1 + } + } + }, "requests": { "type": "object", "properties": { diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml index d9fabb523..0dbd6254f 100644 --- a/utils/helm/speckle-server/values.yaml +++ b/utils/helm/speckle-server/values.yaml @@ -771,6 +771,13 @@ frontend_2: ## @param frontend_2.replicas The number of instances of the Frontend 2 server prod to be deployed withing the cluster. ## replicas: 1 + onboarding: + ## @param frontend_2.onboarding.stream_url The (cross-server) URL to the project/stream that should be used as the onboarding project base. + ## + stream_url: 'https://latest.speckle.systems/projects/843d07eb10' + ## @param frontend_2.onboarding.stream_cache_bust_number Increase this number to trigger the re-pulling of the base stream + ## + stream_cache_bust_number: 1 requests: ## @param frontend_2.requests.cpu The CPU that should be available on a node when scheduling this pod. ## ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ From f79740b3a0ba76d655b9513346657a345a6814e2 Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Mon, 31 Jul 2023 12:31:55 +0300 Subject: [PATCH 07/11] fix(server): moved apollo client to main deps --- packages/server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/package.json b/packages/server/package.json index ec7cb0ad3..ab997fc11 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -32,6 +32,7 @@ "gqlgen:watch": "graphql-codegen --config codegen.yml --watch \"assets/**/*.graphql\"" }, "dependencies": { + "@apollo/client": "^3.7.0", "@aws-sdk/client-s3": "^3.276.0", "@aws-sdk/lib-storage": "^3.100.0", "@godaddy/terminus": "^4.9.0", @@ -99,7 +100,6 @@ "zxcvbn": "^4.4.2" }, "devDependencies": { - "@apollo/client": "^3.7.0", "@apollo/rover": "^0.14.1", "@bull-board/express": "^4.2.2", "@faker-js/faker": "^7.1.0", From f2f3f9078681eef85bdff06a6ebe70b72468db7f Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Mon, 31 Jul 2023 12:58:46 +0300 Subject: [PATCH 08/11] fix(server): moved cross-fetch to main deps --- packages/server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/package.json b/packages/server/package.json index ab997fc11..e42dd9e46 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -51,6 +51,7 @@ "compression": "^1.7.4", "connect-redis": "^6.1.1", "cors": "^2.8.5", + "cross-fetch": "^3.1.5", "crypto-random-string": "^3.2.0", "dataloader": "^2.0.0", "dayjs": "^1.11.5", @@ -138,7 +139,6 @@ "chai-http": "^4.3.0", "concurrently": "^7.0.0", "cross-env": "^7.0.3", - "cross-fetch": "^3.1.5", "deep-equal-in-any-order": "^1.1.15", "eslint": "^8.11.0", "eslint-config-prettier": "^8.5.0", From 7bebdd1890c28aa2f2544a72377c93e60a932e89 Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Mon, 31 Jul 2023 13:32:28 +0300 Subject: [PATCH 09/11] fix(server): moved objectloader to main deps --- packages/server/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index e42dd9e46..d81faaef6 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -98,7 +98,8 @@ "undici": "^5.19.1", "verror": "^1.10.1", "xml-escape": "^1.1.0", - "zxcvbn": "^4.4.2" + "zxcvbn": "^4.4.2", + "@speckle/objectloader": "workspace:^" }, "devDependencies": { "@apollo/rover": "^0.14.1", @@ -108,7 +109,6 @@ "@graphql-codegen/typescript": "2.7.2", "@graphql-codegen/typescript-operations": "^2.5.2", "@graphql-codegen/typescript-resolvers": "2.7.2", - "@speckle/objectloader": "workspace:^", "@swc/core": "^1.2.222", "@tiptap/core": "^2.0.0-beta.176", "@types/bcrypt": "^5.0.0", From 87a8e7e01d1db44bbbe016d710d0edeabfaca571 Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Mon, 31 Jul 2023 13:56:29 +0300 Subject: [PATCH 10/11] fix(server): env var definitions --- packages/server/package.json | 4 +-- .../templates/frontend_2/deployment.yml | 5 ---- .../templates/server/deployment.yml | 6 ++-- utils/helm/speckle-server/values.schema.json | 30 +++++++++---------- utils/helm/speckle-server/values.yaml | 15 +++++----- 5 files changed, 28 insertions(+), 32 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index d81faaef6..ab362c95b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -40,6 +40,7 @@ "@mailchimp/mailchimp_marketing": "^3.0.80", "@sentry/node": "^6.17.9", "@sentry/tracing": "^6.17.9", + "@speckle/objectloader": "workspace:^", "@speckle/shared": "workspace:^", "@types/mailchimp__mailchimp_marketing": "^3.0.9", "@types/pino-http": "^5.8.1", @@ -98,8 +99,7 @@ "undici": "^5.19.1", "verror": "^1.10.1", "xml-escape": "^1.1.0", - "zxcvbn": "^4.4.2", - "@speckle/objectloader": "workspace:^" + "zxcvbn": "^4.4.2" }, "devDependencies": { "@apollo/rover": "^0.14.1", diff --git a/utils/helm/speckle-server/templates/frontend_2/deployment.yml b/utils/helm/speckle-server/templates/frontend_2/deployment.yml index 35ef7c515..8008c7fa8 100644 --- a/utils/helm/speckle-server/templates/frontend_2/deployment.yml +++ b/utils/helm/speckle-server/templates/frontend_2/deployment.yml @@ -66,11 +66,6 @@ spec: value: {{ .Values.frontend_2.logClientApiToken }} - name: NUXT_PUBLIC_LOG_CLIENT_API_ENDPOINT value: {{ .Values.frontend_2.logClientApiEndpoint }} - - name: ONBOARDING_STREAM_URL - value: {{ .Values.frontend_2.onboarding.stream_url }} - - name: ONBOARDING_STREAM_CACHE_BUST_NUMBER - value: {{ .Values.frontend_2.onboarding.stream_cache_bust_number }} - priorityClassName: high-priority {{- if .Values.frontend_2.affinity }} diff --git a/utils/helm/speckle-server/templates/server/deployment.yml b/utils/helm/speckle-server/templates/server/deployment.yml index 9d7bd9621..7fc911790 100644 --- a/utils/helm/speckle-server/templates/server/deployment.yml +++ b/utils/helm/speckle-server/templates/server/deployment.yml @@ -108,8 +108,10 @@ spec: value: http://{{ .Values.domain }} {{- end }} - - name: ONBOARDING_STREAM_ID - value: {{ .Values.server.onboardingStreamId }} + - name: ONBOARDING_STREAM_URL + value: {{ .Values.server.onboarding.stream_url }} + - name: ONBOARDING_STREAM_CACHE_BUST_NUMBER + value: {{ .Values.server.onboarding.stream_cache_bust_number }} - name: SESSION_SECRET valueFrom: diff --git a/utils/helm/speckle-server/values.schema.json b/utils/helm/speckle-server/values.schema.json index ba5fd5b19..fd3555f81 100644 --- a/utils/helm/speckle-server/values.schema.json +++ b/utils/helm/speckle-server/values.schema.json @@ -466,6 +466,21 @@ "description": "The minimum level of logs which will be output. Suitable values are trace, debug, info, warn, error, fatal, or silent", "default": "info" }, + "onboarding": { + "type": "object", + "properties": { + "stream_url": { + "type": "string", + "description": "The (cross-server) URL to the project/stream that should be used as the onboarding project base.", + "default": "https://latest.speckle.systems/projects/843d07eb10" + }, + "stream_cache_bust_number": { + "type": "number", + "description": "Increase this number to trigger the re-pulling of the base stream", + "default": 1 + } + } + }, "inspect": { "type": "object", "properties": { @@ -1162,21 +1177,6 @@ "description": "The number of instances of the Frontend 2 server prod to be deployed withing the cluster.", "default": 1 }, - "onboarding": { - "type": "object", - "properties": { - "stream_url": { - "type": "string", - "description": "The (cross-server) URL to the project/stream that should be used as the onboarding project base.", - "default": "https://latest.speckle.systems/projects/843d07eb10" - }, - "stream_cache_bust_number": { - "type": "number", - "description": "Increase this number to trigger the re-pulling of the base stream", - "default": 1 - } - } - }, "requests": { "type": "object", "properties": { diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml index 0dbd6254f..c3e63b86d 100644 --- a/utils/helm/speckle-server/values.yaml +++ b/utils/helm/speckle-server/values.yaml @@ -376,7 +376,13 @@ server: ## @param server.logLevel The minimum level of logs which will be output. Suitable values are trace, debug, info, warn, error, fatal, or silent ## logLevel: 'info' - + onboarding: + ## @param server.onboarding.stream_url The (cross-server) URL to the project/stream that should be used as the onboarding project base. + ## + stream_url: 'https://latest.speckle.systems/projects/843d07eb10' + ## @param server.onboarding.stream_cache_bust_number Increase this number to trigger the re-pulling of the base stream + ## + stream_cache_bust_number: 1 inspect: ## @param server.inspect.enabled If enabled, indicates that the Speckle server should be deployed with the nodejs inspect feature enabled enabled: false @@ -771,13 +777,6 @@ frontend_2: ## @param frontend_2.replicas The number of instances of the Frontend 2 server prod to be deployed withing the cluster. ## replicas: 1 - onboarding: - ## @param frontend_2.onboarding.stream_url The (cross-server) URL to the project/stream that should be used as the onboarding project base. - ## - stream_url: 'https://latest.speckle.systems/projects/843d07eb10' - ## @param frontend_2.onboarding.stream_cache_bust_number Increase this number to trigger the re-pulling of the base stream - ## - stream_cache_bust_number: 1 requests: ## @param frontend_2.requests.cpu The CPU that should be available on a node when scheduling this pod. ## ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ From 3e5b5b9e3014f990d59dd16b5c24dee62d5f22ca Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Mon, 31 Jul 2023 14:46:16 +0300 Subject: [PATCH 11/11] fix(helm): quoting bust number --- utils/helm/speckle-server/templates/server/deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/helm/speckle-server/templates/server/deployment.yml b/utils/helm/speckle-server/templates/server/deployment.yml index 7fc911790..75d501a5c 100644 --- a/utils/helm/speckle-server/templates/server/deployment.yml +++ b/utils/helm/speckle-server/templates/server/deployment.yml @@ -111,7 +111,7 @@ spec: - name: ONBOARDING_STREAM_URL value: {{ .Values.server.onboarding.stream_url }} - name: ONBOARDING_STREAM_CACHE_BUST_NUMBER - value: {{ .Values.server.onboarding.stream_cache_bust_number }} + value: {{ .Values.server.onboarding.stream_cache_bust_number | quote }} - name: SESSION_SECRET valueFrom: