From 9d5256dbce158d407e0f8fdb39f73a1ba4cba002 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Thu, 19 Dec 2024 12:11:53 -0500 Subject: [PATCH 1/3] Checkpoint --- .../io/xh/hoist/impl/XhController.groovy | 10 ++++++ .../admin/MemoryMonitoringService.groovy | 18 ++++------- .../xh/hoist/jsonblob/JsonBlobService.groovy | 32 ++++++++++++++----- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/grails-app/controllers/io/xh/hoist/impl/XhController.groovy b/grails-app/controllers/io/xh/hoist/impl/XhController.groovy index 14446044..9f1292b2 100644 --- a/grails-app/controllers/io/xh/hoist/impl/XhController.groovy +++ b/grails-app/controllers/io/xh/hoist/impl/XhController.groovy @@ -176,6 +176,11 @@ class XhController extends BaseController { renderJSON(ret.formatForClient()) } + def findJsonBlob(String type, String name) { + def ret = jsonBlobService.find(type, name) + renderJSON(ret?.formatForClient()) + } + def listJsonBlobs(String type, boolean includeValue) { def ret = jsonBlobService.list(type) renderJSON(ret*.formatForClient(includeValue)) @@ -191,6 +196,11 @@ class XhController extends BaseController { renderJSON(ret.formatForClient()) } + def createOrUpdateJsonBlob(String type, String name, String update) { + def ret = jsonBlobService.createOrUpdate(type, name, parseObject(update)) + renderJSON(ret.formatForClient()) + } + def archiveJsonBlob(String token) { def ret = jsonBlobService.archive(token) renderJSON(ret.formatForClient()) diff --git a/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy b/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy index 6ba54e9d..e2f63b08 100644 --- a/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy +++ b/grails-app/services/io/xh/hoist/admin/MemoryMonitoringService.groovy @@ -40,7 +40,6 @@ class MemoryMonitoringService extends BaseService { private Date _lastInfoLogged private final String blobOwner = 'xhMemoryMonitoringService' private final static String blobType = isProduction ? 'xhMemorySnapshots' : "xhMemorySnapshots_$appEnvironment" - private String blobToken void init() { createTimer( @@ -211,19 +210,14 @@ class MemoryMonitoringService extends BaseService { private void persistSnapshots() { try { - if (blobToken) { - jsonBlobService.update(blobToken, [value: snapshots], blobOwner) - } else { - def blob = jsonBlobService.create([ - name : clusterService.instanceName, - type : blobType, - value: snapshots - ], blobOwner) - blobToken = blob.token - } + jsonBlobService.createOrUpdate( + blobType, + clusterService.instanceName, + [value: snapshots], + blobOwner + ) } catch (Exception e) { logError('Failed to persist memory snapshots', e) - blobToken = null } } diff --git a/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy b/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy index 90718309..fc818158 100644 --- a/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy +++ b/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy @@ -29,6 +29,11 @@ class JsonBlobService extends BaseService implements DataBinder { return ret } + @ReadOnly + JsonBlob find(String type, String name, String username = username) { + JsonBlob.findByTypeAndNameAndOwnerAndArchivedDate(type, name, username, 0) + } + @ReadOnly List list(String type, String username = username) { JsonBlob @@ -48,15 +53,15 @@ class JsonBlobService extends BaseService implements DataBinder { @Transactional JsonBlob update(String token, Map data, String username = username) { def blob = get(token, username) - if (data) { - data = [*: data, lastUpdatedBy: username] - if (data.containsKey('value')) data.value = serialize(data.value) - if (data.containsKey('meta')) data.meta = serialize(data.meta) + return updateInternal(blob, data, username) + } - bindData(blob, data) - blob.save() - } - return blob + @Transactional + JsonBlob createOrUpdate(String type, String name, Map data, String username = username) { + def blob = find(type, name, username) + return blob ? + updateInternal(blob, data, username) : + create([*: data, type: type, name: name, owner: username], username) } @Transactional @@ -71,6 +76,17 @@ class JsonBlobService extends BaseService implements DataBinder { //------------------------- // Implementation //------------------------- + private JsonBlob updateInternal(JsonBlob blob, Map data, String username) { + if (!data) return blob + data = [*: data, lastUpdatedBy: username] + if (data.containsKey('value')) data.value = serialize(data.value) + if (data.containsKey('meta')) data.meta = serialize(data.meta) + + bindData(blob, data) + blob.save() + return blob + } + private boolean passesAcl(JsonBlob blob, String username) { return blob.acl == '*' || blob.owner == username } From 0d56c4c70d8862b53f45d57cf758878bf6bb69e3 Mon Sep 17 00:00:00 2001 From: lbwexler Date: Tue, 24 Dec 2024 12:11:34 -0500 Subject: [PATCH 2/3] Checkpoint --- .../io/xh/hoist/impl/XhController.groovy | 4 +- .../io/xh/hoist/impl/XhViewController.groovy | 60 +++++++ .../xh/hoist/jsonblob/JsonBlobService.groovy | 34 ++-- .../io/xh/hoist/view/ViewService.groovy | 164 ++++++++++++++++++ 4 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 grails-app/controllers/io/xh/hoist/impl/XhViewController.groovy create mode 100644 grails-app/services/io/xh/hoist/view/ViewService.groovy diff --git a/grails-app/controllers/io/xh/hoist/impl/XhController.groovy b/grails-app/controllers/io/xh/hoist/impl/XhController.groovy index 9f1292b2..3137d7a2 100644 --- a/grails-app/controllers/io/xh/hoist/impl/XhController.groovy +++ b/grails-app/controllers/io/xh/hoist/impl/XhController.groovy @@ -176,8 +176,8 @@ class XhController extends BaseController { renderJSON(ret.formatForClient()) } - def findJsonBlob(String type, String name) { - def ret = jsonBlobService.find(type, name) + def findJsonBlob(String type, String name, String owner) { + def ret = jsonBlobService.find(type, name, owner) renderJSON(ret?.formatForClient()) } diff --git a/grails-app/controllers/io/xh/hoist/impl/XhViewController.groovy b/grails-app/controllers/io/xh/hoist/impl/XhViewController.groovy new file mode 100644 index 00000000..f803e283 --- /dev/null +++ b/grails-app/controllers/io/xh/hoist/impl/XhViewController.groovy @@ -0,0 +1,60 @@ +/* + * This file belongs to Hoist, an application development toolkit + * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) + * + * Copyright © 2023 Extremely Heavy Industries Inc. + */ + +package io.xh.hoist.impl + +import groovy.transform.CompileStatic +import io.xh.hoist.BaseController +import io.xh.hoist.security.AccessAll +import io.xh.hoist.view.ViewService + +@AccessAll +@CompileStatic +class XhViewController extends BaseController { + + ViewService viewService + + //---------------------------- + // ViewManager state + support + //----------------------------- + def allData(String type, String viewInstance) { + renderJSON(viewService.getAllData(type, viewInstance)) + } + + def updateState(String type, String viewInstance) { + viewService.updateState(type, viewInstance, parseRequestJSON()) + renderJSON([success: true]) + } + + //--------------------------- + // Individual View management + //---------------------------- + def get(String token) { + renderJSON(viewService.get(token)) + } + + def create() { + renderJSON(viewService.create(parseRequestJSON())) + } + + def delete(String tokens) { + viewService.delete(tokens.split(',').toList()) + renderJSON([success: true]) + } + + def updateInfo(String token) { + renderJSON(viewService.updateInfo(token, parseRequestJSON())) + } + + def updateValue(String token) { + renderJSON(viewService.updateValue(token, parseRequestJSON())) + } + + def makeGlobal(String token) { + renderJSON(viewService.makeGlobal(token)) + } +} diff --git a/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy b/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy index fc818158..f5d1a090 100644 --- a/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy +++ b/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy @@ -20,18 +20,16 @@ class JsonBlobService extends BaseService implements DataBinder { @ReadOnly JsonBlob get(String token, String username = username) { JsonBlob ret = JsonBlob.findByTokenAndArchivedDate(token, 0) - if (!ret) { - throw new RuntimeException("Active JsonBlob not found with token '$token'") - } - if (!passesAcl(ret, username)) { - throw new NotAuthorizedException("User '$username' does not have access to JsonBlob with token '$token'") - } + if (!ret) throw new RuntimeException("Active JsonBlob not found with token '$token'") + ensureAccess(ret, username) return ret } @ReadOnly - JsonBlob find(String type, String name, String username = username) { - JsonBlob.findByTypeAndNameAndOwnerAndArchivedDate(type, name, username, 0) + JsonBlob find(String type, String name, String owner, String username = username) { + def ret = JsonBlob.findByTypeAndNameAndOwnerAndArchivedDate(type, name, owner, 0) + if (ret) ensureAccess(ret, username) + return ret } @ReadOnly @@ -41,6 +39,12 @@ class JsonBlobService extends BaseService implements DataBinder { .findAll { passesAcl(it, username) } } + @Transactional + JsonBlob update(String token, Map data, String username = username) { + def blob = get(token, username) + return updateInternal(blob, data, username) + } + @Transactional JsonBlob create(Map data, String username = username) { data = [*: data, owner: username, lastUpdatedBy: username] @@ -50,15 +54,9 @@ class JsonBlobService extends BaseService implements DataBinder { new JsonBlob(data).save() } - @Transactional - JsonBlob update(String token, Map data, String username = username) { - def blob = get(token, username) - return updateInternal(blob, data, username) - } - @Transactional JsonBlob createOrUpdate(String type, String name, Map data, String username = username) { - def blob = find(type, name, username) + def blob = find(type, name, username, username) return blob ? updateInternal(blob, data, username) : create([*: data, type: type, name: name, owner: username], username) @@ -90,4 +88,10 @@ class JsonBlobService extends BaseService implements DataBinder { private boolean passesAcl(JsonBlob blob, String username) { return blob.acl == '*' || blob.owner == username } + + private ensureAccess(JsonBlob blob, String username) { + if (!passesAcl(blob, username)) { + throw new NotAuthorizedException("User '$username' does not have access to JsonBlob with token '${blob.token}'") + } + } } diff --git a/grails-app/services/io/xh/hoist/view/ViewService.groovy b/grails-app/services/io/xh/hoist/view/ViewService.groovy new file mode 100644 index 00000000..f68ffa8f --- /dev/null +++ b/grails-app/services/io/xh/hoist/view/ViewService.groovy @@ -0,0 +1,164 @@ +/* + * This file belongs to Hoist, an application development toolkit + * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) + * + * Copyright © 2023 Extremely Heavy Industries Inc. + */ + +package io.xh.hoist.view + +import groovy.transform.CompileStatic +import io.xh.hoist.BaseService +import io.xh.hoist.jsonblob.JsonBlob +import io.xh.hoist.jsonblob.JsonBlobService +import io.xh.hoist.track.TrackService + +import static io.xh.hoist.json.JSONParser.parseObject + +/** + * Manage all View state for Hoist's built-in client-side views + */ +@CompileStatic +class ViewService extends BaseService { + + JsonBlobService jsonBlobService + TrackService trackService + + static final String STATE_BLOB_NAME = 'xhUserState'; + + //---------------------------- + // ViewManager state + support + //----------------------------- + /** + * Get all the views, and the current view state for a user + */ + Map getAllData(String type, String viewInstance) { + def blobs = jsonBlobService.list(type).split { it.name == STATE_BLOB_NAME } + def (rawState, views) = [blobs[0], blobs[1]] + + // Transform state + rawState = rawState ? parseObject(rawState[0].value) : null + def state = [ + userPinned: rawState?.userPinned ?: [:], + autoSave: rawState?.autoSave ?: false + ] + Map currentView = rawState?.currentView as Map + if (currentView?.containsKey(viewInstance)) { + state.currentView = currentView[viewInstance] + } + + // Transform views + views = views*.formatForClient(false) + + return [state: state, views: views] + } + + /** Update state for this user */ + void updateState(String type, String viewInstance, Map update) { + def currBlob = jsonBlobService.find(type, STATE_BLOB_NAME, username), + currValue = parseObject(currBlob.value), + newValue = [ + currentView: currValue?.currentView ?: [:], + userPinned : currValue?.userPinned ?: [:], + autoSave : currValue?.autoSave ?: false + ] + + if (update.containsKey('currentView')) newValue.currentView[viewInstance] = update.currentView + if (update.containsKey('userPinned')) newValue.userPinned = update.userPinned + if (update.containsKey('autoSave')) newValue.autoSave = update.autoSave + + jsonBlobService.createOrUpdate(type, STATE_BLOB_NAME, [value: newValue]) + } + + //--------------------------- + // Individual View management + //---------------------------- + /** Fetch the latest version of a view. */ + Map get(String token) { + jsonBlobService.get(token).formatForClient(true) + } + + /** Fetch the latest version of a view. */ + Map create(Map data) { + def ret = jsonBlobService.create( + type: data.type, + name: data.name, + description: data.description, + acl: data.isShared ? '*' : null, + meta: [group: data.group, isShared: data.isShared], + value: data.value + ) + trackChange('Created View', ret) + ret.formatForClient(true) + } + + /** Update a views metadata */ + Map updateInfo(String token, Map data) { + def existing = jsonBlobService.get(token), + existingMeta = parseObject(existing.meta), + isGlobal = existingMeta.isGlobal, + isShared = data.containsKey('isShared') ? data.isShared : existingMeta.isShared; + + def ret = jsonBlobService.update( + token, + [ + *: data, + acl: isGlobal || isShared ? '*' : null, + meta: isGlobal ? + [group: data.group, isDefaultPinned: !!data.isDefaultPinned]: + [group: data.group, isShared: !!data.isShared], + ] + ) + trackChange('Updated View Info', ret) + ret.formatForClient(true) + } + + /** Update a views value */ + Map updateValue(String token, Map value) { + def ret = jsonBlobService.update(token, [value: value]); + if (ret.owner == null) { + trackChange('Updated Global View definition', ret); + } + ret.formatForClient(true) + } + + /** Make a view global */ + Map makeGlobal(String token) { + def existing = jsonBlobService.get(token), + meta = parseObject(existing.meta)?.findAll { it.key != 'isShared' }, + ret = jsonBlobService.update(token, [owner: null, acl: '*', meta: meta]) + + this.trackChange('Made View Global', ret) + ret.formatForClient(true) + } + + + /** Bulk Delete views */ + void delete(List tokens) { + def successCount = 0 + tokens.each { + try { + jsonBlobService.archive(it) + successCount++; + } catch (Exception e) { + logError('Failed to delete View', [token: it], e) + } + } + if (successCount) { + trackChange('Deleted Views', [count: successCount]); + } + } + + //-------------------- + // Implementation + //--------------------- + private trackChange(String msg, Object data) { + trackService.track( + msg: msg, + category: 'Views', + data: data instanceof JsonBlob ? + [name: data.name, token: data.token, isGlobal: data.owner == null] : + data + ) + } +} From a0ec19fd2cbb4452b04f9a2fff4e9e0726211d5a Mon Sep 17 00:00:00 2001 From: lbwexler Date: Fri, 27 Dec 2024 13:11:21 -0500 Subject: [PATCH 3/3] changes from Greg CR --- .../io/xh/hoist/impl/XhController.groovy | 2 +- .../io/xh/hoist/jsonblob/JsonBlobService.groovy | 13 +++++++------ .../services/io/xh/hoist/view/ViewService.groovy | 11 ++++++++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/grails-app/controllers/io/xh/hoist/impl/XhController.groovy b/grails-app/controllers/io/xh/hoist/impl/XhController.groovy index 3137d7a2..06acbdb0 100644 --- a/grails-app/controllers/io/xh/hoist/impl/XhController.groovy +++ b/grails-app/controllers/io/xh/hoist/impl/XhController.groovy @@ -197,7 +197,7 @@ class XhController extends BaseController { } def createOrUpdateJsonBlob(String type, String name, String update) { - def ret = jsonBlobService.createOrUpdate(type, name, parseObject(update)) + def ret = jsonBlobService.createOrUpdate(type, name, parseObject(update)) renderJSON(ret.formatForClient()) } diff --git a/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy b/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy index f5d1a090..b46d335c 100644 --- a/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy +++ b/grails-app/services/io/xh/hoist/jsonblob/JsonBlobService.groovy @@ -75,13 +75,14 @@ class JsonBlobService extends BaseService implements DataBinder { // Implementation //------------------------- private JsonBlob updateInternal(JsonBlob blob, Map data, String username) { - if (!data) return blob - data = [*: data, lastUpdatedBy: username] - if (data.containsKey('value')) data.value = serialize(data.value) - if (data.containsKey('meta')) data.meta = serialize(data.meta) + if (data) { + data = [*: data, lastUpdatedBy: username] + if (data.containsKey('value')) data.value = serialize(data.value) + if (data.containsKey('meta')) data.meta = serialize(data.meta) - bindData(blob, data) - blob.save() + bindData(blob, data) + blob.save() + } return blob } diff --git a/grails-app/services/io/xh/hoist/view/ViewService.groovy b/grails-app/services/io/xh/hoist/view/ViewService.groovy index f68ffa8f..e4cdc387 100644 --- a/grails-app/services/io/xh/hoist/view/ViewService.groovy +++ b/grails-app/services/io/xh/hoist/view/ViewService.groovy @@ -135,17 +135,22 @@ class ViewService extends BaseService { /** Bulk Delete views */ void delete(List tokens) { - def successCount = 0 + List failures = [] tokens.each { try { jsonBlobService.archive(it) - successCount++; } catch (Exception e) { + failures << e logError('Failed to delete View', [token: it], e) } } + def successCount = tokens.size() - failures.size() if (successCount) { - trackChange('Deleted Views', [count: successCount]); + trackChange('Deleted Views', [count: successCount]) + } + + if (failures) { + throw new RuntimeException("Failed to delete ${failures.size()} view(s)", failures.first()) } }