/////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2002-2021, Open Design Alliance (the "Alliance"). // All rights reserved. // // This software and its documentation and related materials are owned by // the Alliance. The software may only be incorporated into application // programs owned by members of the Alliance, subject to a signed // Membership Agreement and Supplemental Software License Agreement with the // Alliance. The structure and organization of this software are the valuable // trade secrets of the Alliance and its suppliers. The software is also // protected by copyright law and international treaty provisions. Application // programs incorporating this software must include the following statement // with their copyright notices: // // This application incorporates Open Design Alliance software pursuant to a // license agreement with Open Design Alliance. // Open Design Alliance Copyright (C) 2002-2021 by Open Design Alliance. // All rights reserved. // // By use of this software, its documentation or related materials, you // acknowledge and accept the above terms. /////////////////////////////////////////////////////////////////////////////// import { EventEmitter2 } from "../Viewer/EventEmitter2"; import { IHttpClient } from "./IHttpClient"; import { HttpClient } from "./HttpClient"; import { ClientEventMap } from "./ClientEvents"; import { Options } from "../Viewer/Options"; import { OptionsEventMap } from "../Viewer/OptionsEvents"; import { Assembly } from "./Assembly"; import { File } from "./File"; import { Job } from "./Job"; import { Project } from "./Project"; import { User } from "./User"; import { downloadProgress, json, text, parseArgs } from "./impl/Utils"; /** * The `Client.js` library class that provides methods to access the [Open Cloud * Server](https://cloud.opendesign.com/docs/index.html#/opencloud_server) resources like * Projects, Files, Issues etc. */ export class Client extends EventEmitter2 { private _options: Options; private _serverUrl: string; private _httpClient: IHttpClient; private _user: User | null; public eventEmitter: EventEmitter2; /** * @param params - An object containing client configuration parameters. * @param params.serverUrl - Open Cloud Server URL. */ constructor(params: { serverUrl?: string; url?: string } = {}) { super(); this.configure(params); this.eventEmitter = this; this._options = new Options(this); this._user = null; } /** * Open Cloud Server URL. Use {@link Client#configure | configure()} to change server URL. * * @readonly */ get serverUrl(): string { return this._serverUrl; } /** * `VisualizeJS` parameters. Changes to these parameters are automatically applied to * `Viewer` instances associated with that client. */ get options(): Options { return this._options; } /** * Change the client configuration parameters. * * @param params - An object containing new configuration parameters. * @param params.serverUrl - Open Cloud Server URL. * @returns Returns a reference to the `Client`. */ configure(params: { serverUrl?: string }): this { this._serverUrl = (params.serverUrl || "").replace(/\/+$/, ""); this._httpClient = new HttpClient(this.serverUrl); this._user = null; return this; } /** * The Object represents server version information. * * @typedef {any} VersionResult * @property {string} version - The server version. * @property {string} hash - Build hash. */ /** * Returns server version. * * @async */ version(): Promise { return this._httpClient .get("/version") .then((response) => response.json()) .then((data) => ({ ...data, server: data.version, client: "CLIENT_JS_VERSION", })); } /** * Register a new user with the specified email and password. * * @async * @param email - User email. Cannot be empty. * @param password - User password. Cannot be empty. Password can only contain letters (a-z, * A-Z), numbers (0-9), and special characters (~!@#$%^&*()_-+={}[]:;"'`<>,.?|/\ ). * @param userName - User name. Cannot be empty or blank if defined. Leave undefined to use * `username` from email. */ registerUser(email: string, password: string, userName?: string): Promise { return this._httpClient .post("/register", { email, password, userName: userName ?? (email + "").split("@").at(0), }) .then((response) => response.json()); } /** * Resend the Confirmation Email to new user. If the user's email is already confirmed, an * exception will be thrown. * * @async * @param email - User email. * @param password - User password. */ resendConfirmationEmail(email: string, password: string): Promise { return this._httpClient .post("/register/email-confirmation", { email, password }) .then((response) => response.json()); } /** * Confirm the user's email. If the user's email is already confirmed, an exception will be thrown. * * @async * @param emailConfirmationId - Confirmation code from the Confirmation Email. */ confirmUserEmail(emailConfirmationId: string): Promise { return this._httpClient .get(`/register/email-confirmation/${emailConfirmationId}`) .then((response) => response.json()); } /** * Log in an existing user using email or user name. * * @async * @param email - An email or user name for authentication request. * @param password - Password for authentication request. */ async signInWithEmail(email: string, password: string): Promise { const credentials = btoa(unescape(encodeURIComponent(email + ":" + password))); this._httpClient.headers = { Authorization: "Basic " + credentials }; const data = await json(this._httpClient.get("/token")); return this.setCurrentUser(data); } /** * Log in an existing user using API Key. * * @async * @param token - An access token for authentication request. See * {@link User#token | User.token} for more details. */ async signInWithToken(token: string): Promise { this._httpClient.headers = { Authorization: token }; const data = await json(this._httpClient.get("/user")); return this.setCurrentUser(data); } // Save the current logged in user information for internal use. private setCurrentUser(data: any): User { this._user = new User(data, this._httpClient); this._httpClient.headers = { Authorization: data.tokenInfo.token }; this._httpClient.signInUserId = this._user.id; return this._user; } private clearCurrentUser(): void { this._user = null; this._httpClient.headers = {}; this._httpClient.signInUserId = ""; } /** * Returns the current logged in user information. Returns `null` if the user is not logged * in or the logged in user has deleted himself. */ getCurrentUser(): User | null { if (this._user && !this._httpClient.signInUserId) this._user = null; return this._user; } /** * Returns a list of server users. Only admins can get a list of users, if the current logged * in user does not have administrator rights, an exception will be thrown. * * @async */ getUsers(): Promise { return json(this._httpClient.get("/users")) .then((array) => array.map((data) => ({ id: data.id, ...data.userBrief }))) .then((array) => array.map((data) => new User(data, this._httpClient))); } /** * Returns the user information. Only admins can get other users, if the current logged in * user does not have administrator rights, hi can only get himself, otherwise an exception * will be thrown. * * @async * @param userId - User ID. */ getUser(userId: string): Promise { if (userId === this._httpClient.signInUserId) { return json(this._httpClient.get("/user")) .then((data) => ({ id: userId, ...data })) .then((data) => new User(data, this._httpClient)); } else { return json(this._httpClient.get(`/users/${userId}`)) .then((data) => ({ id: data.id, ...data.userBrief })) .then((data) => new User(data, this._httpClient)); } } /** * Create a new user. Only admins can create users, if the current logged in user does not * have administrator rights, an exception will be thrown. * * @async * @param email - User email. Cannot be empty. * @param password - User password. Cannot be empty. Password can only contain latin letters * (a-z, A-Z), numbers (0-9), and special characters (~!@#$%^&*()_-+={}[]:;"'`<>,.?|/\ ). * @param params - Additional user data. * @param params.isAdmin - `true` if user is an administrator. * @param params.userName - User name. Cannot be empty or blank if defined. Leave undefined * to use `username` from email. * @param params.firstName - First name. * @param params.lastName - Last name. * @param params.canCreateProject - `true` if user is allowed to create a project. * @param params.projectsLimit - The maximum number of projects that the user can create. * @param params.storageLimit - The size of the file storage available to the user. */ createUser( email: string, password: string, params: { isAdmin?: boolean; userName?: string; firstName?: string; lastName?: string; canCreateProject?: boolean; projectsLimit?: number; storageLimit?: number; } = {} ): Promise { const { isAdmin, userName, ...rest } = params; return json( this._httpClient.post("/users", { isAdmin, userBrief: { ...rest, email, userName: userName ?? (email + "").split("@").at(0), }, password, }) ) .then((data) => ({ id: data.id, ...data.userBrief })) .then((data) => new User(data, this._httpClient)); } /** * Delete a user from the server. Only admins can delete users, if the current logged in user * does not have administrator rights, an exception will be thrown. * * Admins can delete themselves or other admins. An admin can only delete himself if he is * not the last administrator. * * You need to re-login to continue working after deleting the current logged in user. * * @async * @param userId - User ID. * @returns Returns the raw data of a deleted user. */ deleteUser(userId: string): Promise { return json(this._httpClient.delete(`/users/${userId}`)).then((data) => { if (userId === this._httpClient.signInUserId) { this.clearCurrentUser(); } return data; }); } /** * Result for file list. * * @typedef {any} FilesResult * @property {number} allSize - Total number of files the user has access to. * @property {number} start - The starting index in the file list in the request. * @property {number} limit - The maximum number of requested files. * @property {File[]} result - Result file list. * @property {number} size - The number of files in the result list. */ /** * Returns a list of files the user has access to. * * @async * @param start - The starting index in the file list. Used for paging. * @param limit - The maximum number of files that should be returned per request. Used for paging. * @param name - Filter the files by part of the name. * @param ext - Filter the files by extension. Extension can be `dgn`, `dwf`, `dwg`, `dxf`, * `ifc`, `ifczip`, `nwc`, `nwd`, `obj`, `rcs`, `rfa`, `rvt`, `step`, `stl`, `stp`, `vsf`, * or any other drawing or reference file type extension. You can specify multiple * extensions on one `string` by separating them with a "|". * @param ids - List of file IDs to return. You can specify multiple IDs on one `string` by * separating them with a "|". * @param sortByDesc - Allows to specify the descending order of the result. By default, * files are sorted by name in ascending order. * @param {string} sortField - Allows to specify sort field. */ getFiles( start?: number, limit?: number, name?: string, ext?: string | string[], ids?: string | string[], sortByDesc?: boolean, sortField?: string ): Promise<{ allSize: number; start: number; limit: number; result: File[]; size: number }> { const searchParams = new URLSearchParams(); if (start > 0) searchParams.set("start", start.toString()); if (limit > 0) searchParams.set("limit", limit.toString()); if (name) searchParams.set("name", name); if (ext) { if (Array.isArray(ext)) ext = ext.join("|"); if (typeof ext === "string") ext = ext.toLowerCase(); if (ext) searchParams.set("ext", ext); } if (ids) { if (Array.isArray(ids)) ids = ids.join("|"); searchParams.set("id", ids); } if (sortByDesc !== undefined) searchParams.set("sortBy", sortByDesc ? "desc" : "asc"); if (sortField) searchParams.set("sortField", sortField); let queryString = searchParams.toString(); if (queryString) queryString = "?" + queryString; return json(this._httpClient.get(`/files${queryString}`)).then((files) => { return { ...files, result: files.result.map((data) => new File(data, this._httpClient)), }; }); } /** * Returns the file information. * * @async * @param fileId - File ID. */ getFile(fileId: string): Promise { return json(this._httpClient.get(`/files/${fileId}`)).then((data) => new File(data, this._httpClient)); } /** * Upload a drawing or reference file to the server. * * Fires: * * - {@link UploadProgressEvent | uploadprogress} * * @async * @param file - Web API File object are generally retrieved from a FileList * object returned as a result of a user selecting files using the HTML `` element. * @param params - An object containing upload parameters. * @param params.geometry=true - Create job to extract file geometry data. Can be: * * - `true` - Extract file geometry data into type, defined by [options]{@link Client#options}. * - `vsfx` - Extract file geometry data into `VSFX` to open the file in `VisualizeJS` viewer. * - `gltf` - Extract file geometry data into `glTF` to open the file in `Three.js` viewer. * * @param params.properties=false - Create job to extract file properties. * @param params.waitForDone=false - Wait for geometry and properties jobs to complete. * @param params.timeout - The time, in milliseconds that the function should wait jobs. If * no one jobs are done during this time, the `TimeoutError` exception will be thrown. * @param params.interval - The time, in milliseconds, the function should delay in between * checking jobs status. * @param params.signal - An AbortController * signal object instance, which can be used to abort waiting as desired. * @param params.onProgress - Upload progress callback. */ async uploadFile( file: globalThis.File, params: { geometry?: boolean | string; properties?: boolean; waitForDone?: boolean; timeout?: number; interval?: number; signal?: AbortSignal; onProgress?: (progress: number, file: globalThis.File) => void; } = { geometry: true, properties: false, waitForDone: false, } ): Promise { const result = await this._httpClient .postFile("/files", file, (progress) => { this.emitEvent({ type: "uploadprogress", data: progress, file }); params.onProgress?.(progress, file); }) .then((xhr: XMLHttpRequest) => JSON.parse(xhr.responseText)) .then((data) => new File(data, this._httpClient)); const geometryType = typeof params.geometry === "string" ? params.geometry : this.options.geometryType; const jobs: string[] = []; if (params.geometry) jobs.push((await result.extractGeometry(geometryType)).outputFormat); if (params.properties) jobs.push((await result.extractProperties()).outputFormat); if (params.waitForDone && jobs.length > 0) await result.waitForDone(jobs, true, params); return result; } /** * Delete the drawing or reference file from the server. * * @async * @param fileId - File ID. * @returns Returns the raw data of a deleted file. */ deleteFile(fileId: string): Promise { return json(this._httpClient.delete(`/files/${fileId}`)); } /** * Download the drawing or reference file. * * @async * @param fileId - File ID. * @param onProgress - Download progress callback. * @param signal - An AbortSignal object instance. Allows to communicate with a fetch request * and abort it if desired. */ downloadFile(fileId: string, onProgress?: (progress: number) => void, signal?: AbortSignal): Promise { return this._httpClient .get(`/files/${fileId}/downloads`, signal) .then((response) => downloadProgress(response, onProgress)) .then((response) => response.arrayBuffer()); } /** * Result for job list. * * @typedef {any} JobsResult * @property {number} allSize - Total number of jobs created by the user. * @property {number} start - The starting index in the job list in the request. * @property {number} limit - The maximum number of requested jobs. * @property {Job[]} result - Result job list. * @property {number} size - The number of jobs in the result list. */ /** * Returns a list of jobs created by current user. * * @async * @param status - Filter the jobs by status. Status can be `waiting`, `inpogress`, `done` or * `failed`. You can specify multiple statuses on one `string` by separating them with a "|". * @param limit - The maximum number of jobs that should be returned per request. Used for paging. * @param start - The starting index in the job list. Used for paging. * @param sortByDesc - Allows to specify the descending order of the result. By default, jobs * are sorted by creation time in ascending order. * @param {boolean} sortField - Allows to specify sort field. */ getJobs( status?: string | string[], limit?: number, start?: number, sortByDesc?: boolean, sortField?: string ): Promise<{ allSize: number; start: number; limit: number; result: Job[]; size: number }> { const searchParams = new URLSearchParams(); if (start > 0) searchParams.set("start", start.toString()); if (limit > 0) searchParams.set("limit", limit.toString()); if (status) { if (Array.isArray(status)) status = status.join("|"); if (typeof status === "string") status = status.trim().toLowerCase(); if (status) searchParams.set("status", status); } if (sortByDesc !== undefined) searchParams.set("sortBy", sortByDesc ? "desc" : "asc"); if (sortField) searchParams.set("sortField", sortField); let queryString = searchParams.toString(); if (queryString) queryString = "?" + queryString; return json(this._httpClient.get(`/jobs${queryString}`)).then((jobs) => ({ ...jobs, result: jobs.result.map((data) => new Job(data, this._httpClient)), })); } /** * Returns the job information. * * @async * @param jobId - Job ID. */ getJob(jobId: string): Promise { return json(this._httpClient.get(`/jobs/${jobId}`)).then((data) => new Job(data, this._httpClient)); } /** * Create a new job. * * @async * @param fileId - File ID. * @param outputFormat - The job type. Can be one of: * * - `geometry` - Extract file geometry data into `VSFX`. * - `geometryGltf` - Extract file geometry data into `glTF`. * - `properties` - Extract file properties. * - `validation` - Validate the file. Only for `IFC`. * - `dwg`, `obj`, `gltf`, `glb`, `vsf`, `pdf`, `3dpdf` - Export file to the one of the supported format. * - Other custom job name. Custom job runner must be registered in the job templates table * before creating a job. * * @param parameters - Parameters for the job runner. Can be given as command line arguments * for the File Converter tool in form "--arg=value". */ createJob(fileId: string, outputFormat: string, parameters?: string | object): Promise { return this._httpClient .post("/jobs", { fileId, outputFormat, parameters: parseArgs(parameters), }) .then((response) => response.json()) .then((data) => new Job(data, this._httpClient)); } /** * Remove a job from the server job list. The method does not delete or stop jobs that are * already being executed. * * @async * @param jobId - Job ID. * @returns Returns the raw data of a deleted job. */ deleteJob(jobId: string): Promise { return json(this._httpClient.delete(`/jobs/${jobId}`)); } /** * Result for assembly list. * * @typedef {any} AssembliesResult * @property {number} allSize - Total number of assemblies the user has access to. * @property {number} start - The starting index in the assembly list in the request. * @property {number} limit - The maximum number of requested assemblies. * @property {Assembly[]} result - Result assembly list. * @property {number} size - The number of assemblies in the result list. */ /** * Returns a list of assemblies the user has access to. * * @async * @param start - The starting index in the assembly list. Used for paging. * @param limit - The maximum number of assemblies that should be returned per request. Used * for paging. * @param name - Filter the assemblies by part of the name. * @param ids - List of assembly IDs to return. You can specify multiple IDs on one `string` * by separating them with a "|". * @param sortByDesc - Allows to specify the descending order of the result. By default * assemblies are sorted by name in ascending order. * @param sortField - Allows to specify sort field. */ getAssemblies( start?: number, limit?: number, name?: string, ids?: string | string[], sortByDesc?: boolean, sortField?: string ): Promise<{ allSize: number; start: number; limit: number; result: Assembly[]; size: number }> { const searchParams = new URLSearchParams(); if (start > 0) searchParams.set("start", start.toString()); if (limit > 0) searchParams.set("limit", limit.toString()); if (name) searchParams.set("name", name); if (ids) { if (Array.isArray(ids)) ids = ids.join("|"); if (typeof ids === "string") ids = ids.trim(); if (ids) searchParams.set("id", ids); } if (sortByDesc !== undefined) searchParams.set("sortBy", sortByDesc ? "desc" : "asc"); if (sortField) searchParams.set("sortField", sortField); let queryString = searchParams.toString(); if (queryString) queryString = "?" + queryString; return json(this._httpClient.get(`/assemblies${queryString}`)).then((assemblies) => { return { ...assemblies, result: assemblies.result.map((data) => new Assembly(data, this._httpClient)), }; }); } /** * Get assembly information. * * @async * @param assemblyId - Assembly ID. */ getAssembly(assemblyId: string): Promise { return json(this._httpClient.get(`/assemblies/${assemblyId}`)).then((data) => new Assembly(data, this._httpClient)); } /** * Create a new assembly. * * @async * @param files - List of file IDs. * @param name - Assembly name. * @param params - An object containing upload parameters. * @param params.waitForDone=false - Wait for assembly to be created. */ createAssembly( files: string[], name: string, params?: { waitForDone?: boolean; timeout?: number; interval?: number; signal?: AbortSignal; } ): Promise { const { waitForDone } = params ?? {}; return this._httpClient .post("/assemblies", { name, files }) .then((response) => response.json()) .then((data) => new Assembly(data, this._httpClient)) .then((result) => (waitForDone ? result.waitForDone(params) : result)); } /** * Delete the assembly from the server. * * @async * @param assemblyId - Assembly ID. * @returns Returns the raw data of a deleted assembly. */ deleteAssembly(assemblyId: string): Promise { return json(this._httpClient.delete(`/assemblies/${assemblyId}`)); } /** * Result for project list. * * @typedef {any} ProjectsResult * @property {number} allSize - Total number of projects the user has access to. * @property {number} start - The starting index in the project list in the request. * @property {number} limit - The maximum number of requested projects. * @property {Project[]} result - Result project list. * @property {number} size - The number of projects in the result list. */ /** * Returns a list of projects the user has access to. * * @async * @param start - The starting index in the project list. Used for paging. * @param limit - The maximum number of projects that should be returned per request. Used for paging. * @param name - Filter the projects by part of the name. * @param ids - List of project IDs to return. You can specify multiple IDs on one `string` * by separating them with a "|". * @param sortByDesc - Allows to specify the descending order of the result. By default * projects are sorted by name in ascending order. */ getProjects( start?: number, limit?: number, name?: string, ids?: string | string[], sortByDesc?: boolean ): Promise<{ allSize: number; start: number; limit: number; result: Project[]; size: number }> { const searchParams = new URLSearchParams(); if (start > 0) searchParams.set("start", start.toString()); if (limit > 0) searchParams.set("limit", limit.toString()); if (name) searchParams.set("name", name); if (ids) { if (Array.isArray(ids)) ids = ids.join("|"); if (typeof ids === "string") ids = ids.trim(); if (ids) searchParams.set("id", ids); } if (sortByDesc !== undefined) searchParams.set("sortBy", sortByDesc ? "desc" : "asc"); let queryString = searchParams.toString(); if (queryString) queryString = "?" + queryString; return json(this._httpClient.get(`/projects${queryString}`)) .then((projects) => { // fix for server 23.5 and below if (Array.isArray(projects)) { let result = projects; if (ids) result = result.filter((x) => ids.includes(x.id)); if (name) result = result.filter((x) => x.name.includes(name)); if (limit > 0) { const begin = start > 0 ? start : 0; result = result.slice(begin, begin + limit); } return { allSize: projects.length, start, limit, result, size: result.length, }; } return projects; }) .then((projects) => { return { ...projects, result: projects.result.map((data) => new Project(data, this._httpClient)), }; }); } /** * Returns the project information. * * @async * @param projectId - Project ID. */ getProject(projectId: string): Promise { return json(this._httpClient.get(`/projects/${projectId}`)).then((data) => new Project(data, this._httpClient)); } /** * Create a new project. * * @async * @param name - Project name. * @param description - Project description. * @param startDate - Project start date. * @param endDate - Project end date. * @param avatarUrl - Project preview image URL. Can be Data URL. */ createProject( name: string, description?: string, startDate?: Date | string, endDate?: Date | string, avatarUrl?: string ): Promise { return json( this._httpClient.post("/projects", { name, description, startDate: startDate instanceof Date ? startDate.toISOString() : startDate, endDate: endDate instanceof Date ? endDate.toISOString() : endDate, avatarUrl, }) ).then((data) => new Project(data, this._httpClient)); } /** * Delete the project from the server. * * @async * @param projectId - Project ID. * @returns Returns the raw data of a deleted project. */ deleteProject(projectId: string): Promise { return text(this._httpClient.delete(`/projects/${projectId}`)).then((text) => { // fix for server 23.5 and below try { return JSON.parse(text); } catch { return { id: projectId }; } }); } }