From 318abef710a7c7642138edc03cafcbbff8a41681 Mon Sep 17 00:00:00 2001 From: Harvey Kandola Date: Sun, 8 Oct 2017 20:53:25 -0400 Subject: [PATCH] bulk data load methods for space and document views --- README.md | 18 ++--- domain/category/endpoint.go | 64 ++++++++++++++++++ domain/document/endpoint.go | 103 +++++++++++++++++++++++++++++ gui/app/pods/document/route.js | 41 +++++++----- gui/app/pods/folder/index/route.js | 31 +++++++-- gui/app/services/category.js | 29 ++++++++ gui/app/services/document.js | 46 +++++++++++++ model/category/category.go | 8 +++ server/routing/routes.go | 4 ++ 9 files changed, 315 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 3e404add..509a5d15 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ # Documize Community Edition +## The mission + +To bring software development inspired features to the world of documenting -- refactoring, importing, testing, linting, metrics, PRs, versioning.... + ## What is it? -Documize is an intelligent document environment (IDE) for creating, securing and sharing documents -- everything yoyu need in one place. +Documize is an intelligent document environment (IDE) for creating, securing and sharing documents -- everything you need in one place. ## Why should I care? -Because maybe like us you are tired of: +Because maybe like us, you might be tired of: * juggling WYSIWYG editors, wiki software and various document related solutions -* playing email tennis with document versions, contributions and feedback +* playing document related email tennis with contributions, versions and feedback * sharing not-so-secure folders with external participants Sound familiar? Read on. @@ -18,10 +22,10 @@ Sound familiar? Read on. Anyone who wants a single place for any kind of document. -Anyone who wants to loop in external participants without leaking information. +Anyone who wants to loop in external participants complete security. Anyone who wishes documentation and knowledge capture worked like agile software development. - + ## What's different about Documize? Sane organization through personal, team and public spaces. @@ -83,10 +87,6 @@ Documize is compatible with Auth0 identity as a service. Open Source Identity and Access Management -## The mission - -To bring software development inspired features to the world of documenting -- refactoring, testing, linting, metrics, PRs, versioning.... - ## The legal bit at the end diff --git a/domain/category/endpoint.go b/domain/category/endpoint.go index 524be6ba..f7eff762 100644 --- a/domain/category/endpoint.go +++ b/domain/category/endpoint.go @@ -112,6 +112,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { cat, err := h.Store.Category.GetBySpace(ctx, spaceID) if err != nil && err != sql.ErrNoRows { + h.Runtime.Log.Error("get space categories visible to user failed", err) response.WriteServerError(w, method, err) return } @@ -460,3 +461,66 @@ func (h *Handler) GetSpaceCategoryMembers(w http.ResponseWriter, r *http.Request response.WriteJSON(w, cat) } + +// FetchSpaceData returns: +// 1. categories that user can see for given space +// 2. summary data for each category +// 3. category viewing membership records +func (h *Handler) FetchSpaceData(w http.ResponseWriter, r *http.Request) { + method := "category.FetchSpaceData" + ctx := domain.GetRequestContext(r) + + spaceID := request.Param(r, "spaceID") + if len(spaceID) == 0 { + response.WriteMissingDataError(w, method, "spaceID") + return + } + + ok := permission.HasPermission(ctx, *h.Store, spaceID, pm.SpaceManage, pm.SpaceOwner, pm.SpaceView) + if !ok { + response.WriteForbiddenError(w) + return + } + + fetch := category.FetchSpaceModel{} + + // get space categories visible to user + cat, err := h.Store.Category.GetBySpace(ctx, spaceID) + if err != nil && err != sql.ErrNoRows { + h.Runtime.Log.Error("get space categories visible to user failed", err) + response.WriteServerError(w, method, err) + return + } + if len(cat) == 0 { + cat = []category.Category{} + } + + // summary of space category usage + summary, err := h.Store.Category.GetSpaceCategorySummary(ctx, spaceID) + if err != nil { + h.Runtime.Log.Error("get space category summary failed", err) + response.WriteServerError(w, method, err) + return + } + if len(summary) == 0 { + summary = []category.SummaryModel{} + } + + // get category membership records + member, err := h.Store.Category.GetSpaceCategoryMembership(ctx, spaceID) + if err != nil && err != sql.ErrNoRows { + h.Runtime.Log.Error("get document category membership for space", err) + response.WriteServerError(w, method, err) + return + } + + if len(member) == 0 { + member = []category.Member{} + } + + fetch.Category = cat + fetch.Summary = summary + fetch.Membership = member + + response.WriteJSON(w, fetch) +} diff --git a/domain/document/endpoint.go b/domain/document/endpoint.go index 5b8fccc2..165a3515 100644 --- a/domain/document/endpoint.go +++ b/domain/document/endpoint.go @@ -29,7 +29,9 @@ import ( "github.com/documize/community/model/audit" "github.com/documize/community/model/doc" "github.com/documize/community/model/link" + pm "github.com/documize/community/model/permission" "github.com/documize/community/model/search" + "github.com/documize/community/model/space" ) // Handler contains the runtime information such as logging and database. @@ -371,3 +373,104 @@ func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) { response.WriteJSON(w, results) } + +// FetchDocumentData returns all document data in single API call. +func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) { + method := "document.FetchDocumentData" + ctx := domain.GetRequestContext(r) + + id := request.Param(r, "documentID") + if len(id) == 0 { + response.WriteMissingDataError(w, method, "documentID") + return + } + + // document + document, err := h.Store.Document.Get(ctx, id) + if err == sql.ErrNoRows { + response.WriteNotFoundError(w, method, id) + return + } + if err != nil { + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + if !permission.CanViewSpaceDocument(ctx, *h.Store, document.LabelID) { + response.WriteForbiddenError(w) + return + } + + // permissions + perms, err := h.Store.Permission.GetUserSpacePermissions(ctx, document.LabelID) + if err != nil && err != sql.ErrNoRows { + response.WriteServerError(w, method, err) + return + } + if len(perms) == 0 { + perms = []pm.Permission{} + } + + record := pm.DecodeUserPermissions(perms) + + // links + l, err := h.Store.Link.GetDocumentOutboundLinks(ctx, id) + if len(l) == 0 { + l = []link.Link{} + } + if err != nil && err != sql.ErrNoRows { + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + // spaces + sp, err := h.Store.Space.GetViewable(ctx) + if err != nil && err != sql.ErrNoRows { + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + if len(sp) == 0 { + sp = []space.Space{} + } + + data := documentData{} + data.Document = document + data.Permissions = record + data.Links = l + data.Spaces = sp + + ctx.Transaction, err = h.Runtime.Db.Beginx() + if err != nil { + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{ + LabelID: document.LabelID, + SourceID: document.RefID, + SourceType: activity.SourceTypeDocument, + ActivityType: activity.TypeRead}) + + if err != nil { + h.Runtime.Log.Error(method, err) + } + + h.Store.Audit.Record(ctx, audit.EventTypeDocumentView) + + ctx.Transaction.Commit() + + response.WriteJSON(w, data) +} + +// documentData represents all data associated for a single document. +// Used by FetchDocumentData() bulk data load call. +type documentData struct { + Document doc.Document `json:"document"` + Permissions pm.Record `json:"permissions"` + Spaces []space.Space `json:"folders"` + Links []link.Link `json:"link"` +} diff --git a/gui/app/pods/document/route.js b/gui/app/pods/document/route.js index 42a46935..50e61a18 100644 --- a/gui/app/pods/document/route.js +++ b/gui/app/pods/document/route.js @@ -24,22 +24,31 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, { this.set('documentId', this.paramsFor('document').document_id); return new Ember.RSVP.Promise((resolve) => { - this.get('documentService').getDocument(this.get('documentId')).then((document) => { - this.set('document', document); - - this.get('folderService').getAll().then((folders) => { - this.set('folders', folders); - - this.get('folderService').getFolder(this.get('folderId')).then((folder) => { - this.set('folder', folder); - - this.get('folderService').setCurrentFolder(folder).then(() => { - this.set('permissions', this.get('folderService').get('permissions')); - resolve(); - }); - }); - }); + this.get('documentService').fetchDocumentData(this.get('documentId')).then((data) => { + this.set('document', data.document); + this.set('folders', data.folders); + this.set('folder', data.folder); + this.set('permissions', data.permissions); + this.set('links', data.links); + resolve(); }); + + // this.get('documentService').getDocument(this.get('documentId')).then((document) => { + // this.set('document', document); + + // this.get('folderService').getAll().then((folders) => { + // this.set('folders', folders); + + // this.get('folderService').getFolder(this.get('folderId')).then((folder) => { + // this.set('folder', folder); + + // this.get('folderService').setCurrentFolder(folder).then(() => { + // this.set('permissions', this.get('folderService').get('permissions')); + // resolve(); + // }); + // }); + // }); + // }); }); }, @@ -50,7 +59,7 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, { document: this.get('document'), page: this.get('pageId'), permissions: this.get('permissions'), - links: this.get('linkService').getDocumentLinks(this.get('documentId')), + links: this.get('links'), sections: this.get('sectionService').getAll() }); }, diff --git a/gui/app/pods/folder/index/route.js b/gui/app/pods/folder/index/route.js index ababe826..6ec1831c 100644 --- a/gui/app/pods/folder/index/route.js +++ b/gui/app/pods/folder/index/route.js @@ -15,6 +15,18 @@ import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-rout export default Ember.Route.extend(AuthenticatedRouteMixin, { categoryService: Ember.inject.service('category'), + beforeModel() { + return new Ember.RSVP.Promise((resolve) => { + this.get('categoryService').fetchSpaceData(this.modelFor('folder').folder.get('id')).then((data) => { + this.set('categories', data.category); + this.set('categorySummary', data.summary); + this.set('categoryMembers', data.membership); + + resolve(data); + }); + }); + }, + model() { this.get('browser').setTitle(this.modelFor('folder').folder.get('name')); @@ -25,14 +37,25 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, { documents: this.modelFor('folder').documents, templates: this.modelFor('folder').templates, showStartDocument: false, - categories: this.get('categoryService').getUserVisible(this.modelFor('folder').folder.get('id')), - categorySummary: this.get('categoryService').getSummary(this.modelFor('folder').folder.get('id')), - categoryMembers: this.get('categoryService').getSpaceCategoryMembership(this.modelFor('folder').folder.get('id')), - rootDocCount: 0 + rootDocCount: 0, + categories: this.get('categories'), + categorySummary: this.get('categorySummary'), + categoryMembers: this.get('categoryMembers'), + // categories: this.get('categoryService').getUserVisible(this.modelFor('folder').folder.get('id')), + // categorySummary: this.get('categoryService').getSummary(this.modelFor('folder').folder.get('id')), + // categoryMembers: this.get('categoryService').getSpaceCategoryMembership(this.modelFor('folder').folder.get('id')), }); }, afterModel(model, transition) { // eslint-disable-line no-unused-vars + // model.folder = this.modelFor('folder').folder; + // model.permissions = this.modelFor('folder').permissions; + // model.folders = this.modelFor('folder').folders; + // model.documents = this.modelFor('folder').documents; + // model.templates = this.modelFor('folder').templates; + // model.showStartDocument = false; + // model.rootDocCount = 0; + let docs = model.documents; let categoryMembers = model.categoryMembers; let rootDocCount = 0; diff --git a/gui/app/services/category.js b/gui/app/services/category.js index 5062830d..368cd881 100644 --- a/gui/app/services/category.js +++ b/gui/app/services/category.js @@ -151,5 +151,34 @@ export default BaseService.extend({ }).then((response) => { return response; }); + }, + + // fetchXXX represents UI specific bulk data loading designed to + // reduce network traffic and boost app performance. + // This method that returns: + // 1. getUserVisible() + // 2. getSummary() + // 3. getSpaceCategoryMembership() + fetchSpaceData(spaceId) { + return this.get('ajax').request(`fetch/category/space/${spaceId}`, { + method: 'GET' + }).then((response) => { + let data = { + category: [], + membership: [], + summary: [] + }; + + let cats = response.category.map((obj) => { + let data = this.get('store').normalize('category', obj); + return this.get('store').push(data); + }); + + data.category = cats; + data.membership = response.membership; + data.summary = response.summary; + + return data; + }); } }); diff --git a/gui/app/services/document.js b/gui/app/services/document.js index e14cc6c5..6a920396 100644 --- a/gui/app/services/document.js +++ b/gui/app/services/document.js @@ -17,6 +17,7 @@ const { export default Ember.Service.extend({ sessionService: service('session'), + folderService: service('folder'), ajax: service(), store: service(), @@ -308,6 +309,51 @@ export default Ember.Service.extend({ let data = this.get('store').normalize('page', response); return this.get('store').push(data); }); + }, + + //************************************************** + // Fetch bulk data + //************************************************** + + // fetchXXX represents UI specific bulk data loading designed to + // reduce network traffic and boost app performance. + // This method that returns: + // 1. getUserVisible() + // 2. getSummary() + // 3. getSpaceCategoryMembership() + fetchDocumentData(documentId) { + return this.get('ajax').request(`fetch/document/${documentId}`, { + method: 'GET' + }).then((response) => { + let data = { + document: {}, + permissions: {}, + folders: [], + folder: {}, + links: [], + }; + + let doc = this.get('store').normalize('document', response.document); + doc = this.get('store').push(doc); + + let perms = this.get('store').normalize('space-permission', response); + perms= this.get('store').push(perms); + this.get('folderService').set('permissions', perms); + + let folders = response.folders.map((obj) => { + let data = this.get('store').normalize('folder', obj); + return this.get('store').push(data); + }); + + data.document = doc; + data.permissions = perms; + data.folders = folders; + data.folder = folders.findBy('id', doc.get('folderId')); + data.links = response.links; + + return data; + }); + } }); diff --git a/model/category/category.go b/model/category/category.go index 97eefdc7..5bb03b84 100644 --- a/model/category/category.go +++ b/model/category/category.go @@ -36,3 +36,11 @@ type SummaryModel struct { CategoryID string `json:"categoryId"` Count int64 `json:"count"` } + +// FetchSpaceModel represents categories, summary and membership in a single payload. +// Designed to speed up front-end app. +type FetchSpaceModel struct { + Category []Category `json:"category"` + Summary []SummaryModel `json:"summary"` + Membership []Member `json:"membership"` +} diff --git a/server/routing/routes.go b/server/routing/routes.go index 5640b2c0..10dbd68f 100644 --- a/server/routing/routes.go +++ b/server/routing/routes.go @@ -179,6 +179,10 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) { Add(rt, RoutePrefixPrivate, "pin/{userID}/sequence", []string{"POST", "OPTIONS"}, nil, pin.UpdatePinSequence) Add(rt, RoutePrefixPrivate, "pin/{userID}/{pinID}", []string{"DELETE", "OPTIONS"}, nil, pin.DeleteUserPin) + // fetch methods exist to speed up UI rendering by returning data in bulk + Add(rt, RoutePrefixPrivate, "fetch/category/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.FetchSpaceData) + Add(rt, RoutePrefixPrivate, "fetch/document/{documentID}", []string{"GET", "OPTIONS"}, nil, document.FetchDocumentData) + Add(rt, RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, meta.RobotsTxt) Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, meta.Sitemap)