import {Id, request, RequestBody, RequestOptions, Response} from '../shared'; import { EndpointNode, EndpointNodeDef, Interface, loadUniverseYaml, NamedInterface, Universe, UniverseInfo } from '../modeling'; import {User} from './user'; import {JSONValue, LayerProfile, readLayerProfile} from '../system'; import {AppRoute, Component, ComponentType, FlexView, ViewComponent, VyzeApp, WebView} from "../app"; export type Languages = 'ts' | 'go'; export interface AccountResponse { [_: string]: any; user: User; token: string; } export interface AppRouteView { [_: string]: any; app: VyzeApp; route: AppRoute; view: ViewComponent; } const defaultRequestOptions: RequestOptions = { request: 'json', response: 'json', } export class ServiceError { constructor(public code: number, public message: string) {} } export class ServiceClient { private decoder = new TextDecoder(); private universeIdCache = new Map(); private universeCache = new Map(); private userCache = new Map(); private _token?: string; private _decoder = new TextDecoder(); public constructor(private url = 'https://api.vyze.io/service') { } public set token(token: string | undefined) { this._token = token; } public get token(): string | undefined { return this._token; } // Account public async login(username: string, password: string): Promise { const endpoint = this.buildUrl(`account/login`); return await this.post(endpoint, { username, password, }, defaultRequestOptions); } public async newAccount(): Promise { const endpoint = this.buildUrl(`account/new`); return await this.post(endpoint, {}, defaultRequestOptions); } // Universe public async getUniverses(): Promise { const endpoint = this.buildUrl(`universe`); return await this.get(endpoint, defaultRequestOptions); } public async resolveUniverse(universeName: string, useCache = true): Promise { if (useCache) { const id = this.universeIdCache.get(universeName); if (id) { return id; } } const endpoint = this.buildUrl(`resolve/universe`, {i: universeName}); const id = await this.get(endpoint, defaultRequestOptions); if (id instanceof ServiceError) { return id; } this.universeIdCache.set(universeName, id); return id; } public async loadUniverse(universeId: Id, useCache = true): Promise { if (useCache) { const universe = this.universeCache.get(universeId); if (universe) { return universe; } } const universeYaml = await this.loadUniverseYaml(universeId); if (universeYaml instanceof ServiceError) { return undefined; } const universe = loadUniverseYaml(universeYaml); if (!universe) { return undefined; } if (universe) { this.universeCache.set(universeId, universe); } return universe; } public async loadUniverseYaml(universeId: Id): Promise { const endpoint = this.buildUrl(`universe/${universeId}/export`, {o: '1'}); const universeYaml = await this.get(endpoint, {request: 'json', response: 'raw'}); if (universeYaml instanceof ServiceError) { return universeYaml; } return this.decoder.decode(universeYaml); } public async updateUniverse(universeId: Id, description: string | undefined, isPublic: boolean | undefined, extensible: boolean | undefined): Promise { const endpoint = this.buildUrl(`universe/${universeId}`); const obj: { [_: string]: string | boolean } = {}; if (typeof description !== 'undefined') { obj['description'] = description; } if (typeof isPublic !== 'undefined') { obj['public'] = isPublic; } if (typeof extensible !== 'undefined') { obj['extensible'] = extensible; } await this.put(endpoint, obj, defaultRequestOptions); } public async createModel(universeId: Id, name: string, object: Id, type: 'model' | 'field' | 'value', description: string): Promise { const endpoint = this.buildUrl(`universe/${universeId}/model`); await this.post(endpoint, { name, object, type, description, }, defaultRequestOptions); } public async deleteModel(universeId: Id, name: string): Promise { const endpoint = this.buildUrl(`universe/${universeId}/model`, {i: name}); await this.delete(endpoint, defaultRequestOptions); } public async updateModel(universeId: Id, name: string, description: string | undefined): Promise { const endpoint = this.buildUrl(`universe/${universeId}/model`, {i: name}); const obj: { [_: string]: string } = {} if (description) { obj['description'] = description; } await this.put(endpoint, obj, defaultRequestOptions); } // Endpoints public async createEndpoint(universeId: Id, endpointNode: EndpointNode): Promise { const definition = endpointNode.getDefinition()?.endpoint; if (!definition || !endpointNode.name) { return undefined; } const endpoint = this.buildUrl(`universe/${universeId}/endpoint`); const def = await this.post(endpoint, { name: endpointNode.name, definition, }, defaultRequestOptions); if (def instanceof ServiceError) { return undefined; } return def; } public async updateEndpoint(universeId: Id, endpointNode: EndpointNode): Promise { const definition = endpointNode.getDefinition()?.endpoint; if (!definition || !endpointNode.name) { return undefined; } const endpoint = this.buildUrl(`universe/${universeId}/endpoint`); const def = await this.put(endpoint, { name: endpointNode.name, definition, }, defaultRequestOptions); if (def instanceof ServiceError) { return undefined; } return def; } public async deleteEndpoint(universeId: Id, name: string): Promise { const endpoint = this.buildUrl(`universe/${universeId}/endpoint`, {i: name}); await this.delete(endpoint, defaultRequestOptions); } public async getEndpointInterfaces(universeId: Id, endpointName: string): Promise { const endpoint = this.buildUrl(`universe/${universeId}/endpoint/${endpointName}/interfaces`); return await this.get(endpoint, defaultRequestOptions); } // Interfaces public async createInterface(universeId: Id, namedInterface: NamedInterface): Promise { const endpoint = this.buildUrl(`universe/${universeId}/interface`); return await this.post(endpoint, { name: namedInterface.name, definition: namedInterface, }, defaultRequestOptions); } public async deleteInterface(universeId: Id, name: string): Promise { const endpoint = this.buildUrl(`universe/${universeId}/interface`, {i: name}); return await this.delete(endpoint, defaultRequestOptions); } // JSON schema and code public async getUniverseJSONSchema(universeName: string): Promise { const endpoint = this.buildUrl(`json/${universeName}`); const resp = await this.get(endpoint, {request: 'raw', response: 'raw'}); if (resp instanceof ServiceError) { return resp; } return this.decoder.decode(resp); } public async getUniverseCode(universeName: string, language: Languages): Promise { const endpoint = this.buildUrl(`code/${universeName}`, {l: language}); const resp = await this.get(endpoint, {request: 'raw', response: 'raw'}); if (resp instanceof ServiceError) { return resp; } return this.decoder.decode(resp); } public async getInterfaceCode(universeName: string, interfaceName: string, language: Languages): Promise { const endpoint = this.buildUrl(`code/${universeName}/interface/${interfaceName}`, {l: language}); const resp = await this.get(endpoint, defaultRequestOptions); if (resp instanceof ServiceError) { return resp; } return this.decoder.decode(resp); } public async getEndpointCode(universeName: string, endpointName: string, language: Languages): Promise { const endpoint = this.buildUrl(`code/${universeName}/endpoint/${endpointName}`, {l: language}); const resp = await this.get(endpoint, {request: 'raw', response: 'raw'}); if (resp instanceof ServiceError) { return resp; } return this.decoder.decode(resp); } // Profile public async getLayerProfile(profileId: Id): Promise { const endpoint = this.buildUrl(`profile/${profileId}/tokens`); const profileStr = await this.get(endpoint, {request: 'raw', response: 'raw'}); if (profileStr instanceof ServiceError) { return profileStr; } return readLayerProfile(this.decoder.decode(profileStr)); } // Users public async getUser(userId: Id, useCache = true): Promise { if (useCache) { const user = this.userCache.get(userId); if (user) { return user; } } const endpoint = this.buildUrl(`user/${userId}`); const user = await this.get(endpoint, defaultRequestOptions); if (user instanceof ServiceError) { return user; } this.userCache.set(userId, user); return user; } // Components public async getComponents(universeId?: Id, type?: ComponentType, subType?: string): Promise { const endpoint = this.buildUrl(`component`, { universe: universeId ?? '', type: type ?? '', subType: subType ?? '', }); return await this.get(endpoint, defaultRequestOptions); } public async getComponent(componentId: Id): Promise { const endpoint = this.buildUrl(`component/${componentId}`); return await this.get(endpoint, defaultRequestOptions); } public async getComponentByName(componentName: string): Promise { const endpoint = this.buildUrl(`component/name/${componentName}`); return await this.get(endpoint, defaultRequestOptions); } public async createComponent(name: string, universeId: Id, type: ComponentType, subType: string): Promise { const endpoint = this.buildUrl(`component`); return await this.post(endpoint, { name, universe: universeId, type, subType }, defaultRequestOptions); } public async updateComponent(componentId: Id, name?: string, description?: string, isPublic?: boolean, listed?: boolean, title?: string): Promise { const endpoint = this.buildUrl(`component/${componentId}`); const params: {[_: string]: any} = {}; if (typeof name !== 'undefined') { params['name'] = name; } if (typeof description !== 'undefined') { params['description'] = description; } if (typeof isPublic !== 'undefined') { params['public'] = isPublic; } if (typeof listed !== 'undefined') { params['listed'] = listed; } if (typeof title !== 'undefined') { params['title'] = title; } return await this.put(endpoint, params, defaultRequestOptions); } public async deleteComponent(componentId: Id): Promise { const endpoint = this.buildUrl(`component/${componentId}`); return await this.delete(endpoint, defaultRequestOptions); } public async getView(componentId: Id): Promise { const endpoint = this.buildUrl(`component/view/${componentId}`); const comp = await this.get(endpoint, defaultRequestOptions); return await this.get(endpoint, defaultRequestOptions); } public async updateView(componentId: Id, model?: string, list?: boolean): Promise { const endpoint = this.buildUrl(`component/view/${componentId}`); const params: {[_: string]: any} = {}; if (typeof model !== 'undefined') { params['model'] = model; } if (typeof list !== 'undefined') { params['list'] = list; } return await this.put(endpoint, params, defaultRequestOptions); } public async getWebView(componentId: Id): Promise { const endpoint = this.buildUrl(`component/view/web/${componentId}`); return await this.get(endpoint, defaultRequestOptions); } public async updateWebView(componentId: Id, tag?: string): Promise { const endpoint = this.buildUrl(`component/view/web/${componentId}`); const params: {[_: string]: any} = {}; if (typeof tag !== 'undefined') { params['tag'] = tag; } return await this.put(endpoint, params, defaultRequestOptions); } public async getFlexView(componentId: Id): Promise { const endpoint = this.buildUrl(`component/view/flex/${componentId}`); return await this.get(endpoint, defaultRequestOptions); } public async updateFlexView(componentId: Id, definition?: string): Promise { const endpoint = this.buildUrl(`component/view/flex/${componentId}`); const params: {[_: string]: any} = {}; if (typeof definition !== 'undefined') { params['definition'] = definition; } return await this.put(endpoint, params, defaultRequestOptions); } public async getAppRouteView(appName: string, path: string): Promise { const endpoint = this.buildUrl(`path`, {'a': appName, 'p': path}); return await this.get(endpoint, defaultRequestOptions); } // Apps public async createApp(name: string, universeId: Id, isPublic: boolean): Promise { const endpoint = this.buildUrl(`app`); return await this.post(endpoint, { name, universe: universeId, public: isPublic, }, defaultRequestOptions); } public async getApps(): Promise { const endpoint = this.buildUrl(`app`); return await this.get(endpoint, defaultRequestOptions); } public async getApp(appId: Id): Promise { const endpoint = this.buildUrl(`app/${appId}`); return await this.get(endpoint, defaultRequestOptions); } public async getAppByName(appName: string): Promise { const endpoint = this.buildUrl(`app/name/${appName}`); return await this.get(endpoint, defaultRequestOptions); } public async updateApp(appId: Id, isPublic?: boolean, name?: string, description?: string, path?: string, title?: string): Promise { const endpoint = this.buildUrl(`app/${appId}`); const params: {[_: string]: any} = {}; if (typeof isPublic !== 'undefined') { params['public'] = isPublic; } if (typeof name !== 'undefined') { params['name'] = name; } if (typeof description !== 'undefined') { params['description'] = description; } if (typeof path !== 'undefined') { params['path'] = path; } if (typeof title !== 'undefined') { params['title'] = title; } return await this.put(endpoint, params, defaultRequestOptions); } public async createAppRoute(appId: Id, route: string, viewId: Id): Promise { const endpoint = this.buildUrl(`app/${appId}/routes`); return await this.post(endpoint, { route, view: viewId, }, defaultRequestOptions); } public async getAppRoutes(appId: Id): Promise { const endpoint = this.buildUrl(`app/${appId}/routes`); return await this.get(endpoint, defaultRequestOptions); } public async deleteAppRoute(routeId: Id): Promise { const endpoint = this.buildUrl(`app/routes/${routeId}`); return await this.delete(endpoint, defaultRequestOptions); } // Helpers private buildUrl(endpoint: string, params?: { [key: string]: string | undefined }) { let url = `${this.url}/v1/${endpoint}`; if (params) { const paramStr: string[] = (Array.from(Object.entries(params)) .filter((e) => typeof e[1] !== 'undefined') as [string, string][]) .map((e) => [e[0], encodeURIComponent(e[1])] as [string, string]) .map((e) => `${e[0]}=${e[1]}`); url += '?' + paramStr.join('&'); } return url; } async get(url: string, requestOptions: RequestOptions): Promise { const resp = await this.request('GET', url, undefined, requestOptions); if (resp[1].status !== 200) { return this.handleError(resp[1]); } return resp[0] as T; } async post(url: string, body: JSONValue | ArrayBuffer, requestOptions: RequestOptions): Promise { const resp = await this.request('POST', url, body, requestOptions); if (resp[1].status !== 200) { return this.handleError(resp[1]); } return resp[0] as T; } async put(url: string, body: JSONValue | ArrayBuffer, requestOptions: RequestOptions): Promise { const resp = await this.request('PUT', url, body, requestOptions); if (resp[1].status !== 200) { return this.handleError(resp[1]); } return resp[0] as T; } async delete(url: string, requestOptions: RequestOptions): Promise { const resp = await this.request('DELETE', url, undefined, requestOptions); if (resp[1].status !== 200) { return this.handleError(resp[1]); } return resp[0] as T; } async request( method: 'GET' | 'POST' | 'PUT' | 'DELETE', url: string, body: JSONValue | ArrayBuffer | undefined, options: RequestOptions, headers?: { [_: string]: string }): Promise<[T | undefined, Response]> { let reqBody = body; if (typeof reqBody !== 'undefined' && options.request === 'json') { reqBody = JSON.stringify(reqBody); } const requestHeaders: { [key: string]: string } = {}; if (this._token) { requestHeaders["Authorization"] = `Bearer ${this._token}`; } for (const e of Object.entries(headers ?? {})) { if (typeof e[1] === 'undefined') { continue; } requestHeaders[e[0]] = e[1]; } const response = await request({ method, url, headers: requestHeaders, body: reqBody as RequestBody | undefined }); let respBody = response.body as ArrayBuffer | JSONValue | undefined; if (response.status >= 300) { return [undefined, response]; } if (typeof respBody !== 'undefined' && options.response === 'json') { respBody = JSON.parse(this._decoder.decode(respBody as ArrayBuffer)); } return [respBody as T, response]; } private handleError(resp: Response): ServiceError { if (!resp.body) { return new ServiceError(0, `unknown error, received status code ${resp.status}`); } const text = this._decoder.decode(resp.body); try { const err = JSON.parse(text); return new ServiceError(err['status']['code'], err['status']['message']); } catch (e) { return new ServiceError(0, `unknown error, received status code ${resp.status} and message ${text}`); } } } export function newServiceClient(url = 'https://api.vyze.io/service'): ServiceClient { return new ServiceClient(url); }