From a8519dd434b04a6b70759c6694f65698d3b004f3 Mon Sep 17 00:00:00 2001 From: ijungleboy Date: Sat, 4 Dec 2021 14:22:07 +0100 Subject: [PATCH] prepare sxc.data and sxc.query https://github.com/2sic/2sxc/issues/2619 --- projects/$2sxc/.vscode/settings.json | 4 +- .../sxc-instance/data/sxc-data-query-base.ts | 30 ++ .../$2sxc/src/sxc-instance/data/sxc-data.ts | 75 ++++- .../$2sxc/src/sxc-instance/data/sxc-query.ts | 63 +++++ .../sxc-instance-with-internals.ts | 6 + .../$2sxc/src/sxc-instance/sxc-instance.ts | 257 +++++++++--------- .../src/sxc-instance/web-api/sxc-web-api.ts | 8 - 7 files changed, 297 insertions(+), 146 deletions(-) create mode 100644 projects/$2sxc/src/sxc-instance/data/sxc-data-query-base.ts create mode 100644 projects/$2sxc/src/sxc-instance/data/sxc-query.ts diff --git a/projects/$2sxc/.vscode/settings.json b/projects/$2sxc/.vscode/settings.json index d7207a465..37958f856 100644 --- a/projects/$2sxc/.vscode/settings.json +++ b/projects/$2sxc/.vscode/settings.json @@ -1,3 +1,5 @@ { - "typescript.tsdk": "node_modules\\typescript\\lib" + "typescript.tsdk": "node_modules\\typescript\\lib", + "editor.tabSize": 2, + "editor.detectIndentation": false } \ No newline at end of file diff --git a/projects/$2sxc/src/sxc-instance/data/sxc-data-query-base.ts b/projects/$2sxc/src/sxc-instance/data/sxc-data-query-base.ts new file mode 100644 index 000000000..d2cf61b7b --- /dev/null +++ b/projects/$2sxc/src/sxc-instance/data/sxc-data-query-base.ts @@ -0,0 +1,30 @@ +import { SxcInstance } from ".."; +import { SxcWebApi } from "../web-api/sxc-web-api"; + +/** +* Base class doing common checks +*/ +export class SxcDataQueryBase { + protected readonly webApi: SxcWebApi; + /** + * Creates an instance of SxcData. + * @param {SxcInstance} sxc + * @param {string} name the content-type name + * @memberof SxcData + */ + constructor( + sxc: SxcInstance, + readonly name: string, + nameInError: string + ) { + this.webApi = sxc.webApi; + + // Fail early if something is wrong + nameInError += ' name '; + if (name == null) throw nameInError + 'is empty'; + if (name.indexOf("/") != -1 || name.indexOf("\\") != -1) throw nameInError + 'has slashes - not allowed'; + if (name.indexOf("?") != -1) throw nameInError + 'has "?" - not allowed'; + } + +} + \ No newline at end of file diff --git a/projects/$2sxc/src/sxc-instance/data/sxc-data.ts b/projects/$2sxc/src/sxc-instance/data/sxc-data.ts index 0c3fe5296..60b804831 100644 --- a/projects/$2sxc/src/sxc-instance/data/sxc-data.ts +++ b/projects/$2sxc/src/sxc-instance/data/sxc-data.ts @@ -1,23 +1,68 @@ import { SxcInstance } from ".."; import { SxcWebApi } from "../web-api/sxc-web-api"; +import { SxcDataQueryBase } from './sxc-data-query-base'; -export class SxcData { - private readonly webApi: SxcWebApi; - - constructor( - private readonly sxc: SxcInstance, - readonly contentType: string - ) { - this.webApi = sxc.webApi; +/** +* Instance Data accessor to get (and in future create/update) data items/entities +*/ +export class SxcData extends SxcDataQueryBase { + /** + * Creates an instance of SxcData. + * @param {SxcInstance} sxc + * @param {string} name the content-type name + * @memberof SxcData + */ + constructor(sxc: SxcInstance, readonly name: string) { + super(sxc, name, 'ContentType'); + } - if (contentType == null) throw "contentType is empty"; - if (contentType.indexOf("/") != -1 || contentType.indexOf("\\") != -1) - throw "contentType has slashes - not allowed"; + /** + * Get all items of this type. + */ + getAll(): Promise { + return this.getInternal(); } - - get(ids?: string | number, params?: string | Record): Promise { - let path = "app/auto/content/" + this.contentType; - if (ids && (typeof ids === 'string' || typeof ids === 'number')) path += "/" + ids; + + /** + * Get the specific item with the ID. It will return null if not found + */ + getOne(id: number): Promise | null { + return this.getInternal(id); + }; + + // Future + private getMany(criteria: Record, fields: Array): Promise { + throw 'not implemented - probably v13.5 or something'; + } + + + /** + * Get all or one data entity from the backend + * @param id optional id as number or string - if not provided, will get all + * @param params optional parameters - ATM not usefuly but we plan to support more filters etc. + * @returns an array with 1 or n entities in the simple JSON format + */ + private getInternal(id?: string | number, params?: string | Record): Promise { + let path = "app/auto/content/" + this.name; + if (id && (typeof id === 'string' || typeof id === 'number')) path += "/" + id; return this.webApi.fetchJson(this.webApi.url(path, params)); } + + // TODO: @SPM create + // - Create a type for the `metadataFor` attribute + // - implement create + // - capture various null-cases + // - if `metadataFor` is specified, add attribute `for` the the object before sending (that should work) + + create(values: Record): Promise>; + + create(values: Record, metadataFor?: any): Promise> { + return null; + } + + // TODO: @SPM update + update(id: number, values: Record): Promise> { + return null; + } } + \ No newline at end of file diff --git a/projects/$2sxc/src/sxc-instance/data/sxc-query.ts b/projects/$2sxc/src/sxc-instance/data/sxc-query.ts new file mode 100644 index 000000000..9fcf4fbdc --- /dev/null +++ b/projects/$2sxc/src/sxc-instance/data/sxc-query.ts @@ -0,0 +1,63 @@ +import { SxcInstance } from ".."; +import { SxcDataQueryBase } from './sxc-data-query-base'; + +/** + * Instance Data accessor to get (and in future create/update) data items/entities + */ +export class SxcQuery extends SxcDataQueryBase { + + constructor(sxc: SxcInstance, readonly name: string) { + super(sxc, name, 'Query'); + } + + /** + * Retrieve the entire query with all streams + * + * @template T + * @returns {Promise} containing a object with stream-names and items in the streams. + * @memberof SxcQuery + */ + getAll(): Promise { + return this.getInternal(); + } + + /** + * Get just one stream, returning an array of items in that stream + * + * @template T + * @param {string} stream + * @returns {Promise} containing an array of items - or empty if stream not found or nothing returned + * @memberof SxcQuery + */ + getStream(stream: string): Promise { + if (stream.indexOf(',') !== -1) throw "parameter 'stream' can only contain one stream name for 'getStream'"; + return this.getInternal(stream).then((data) => { + if (data == null || !data.hasOwnProperty(stream)) return []; + return (data as any)[stream] as T[]; + }) + } + + /** + * Get a query but only the selected streams. + * + * @template T + * @param {string} streams + * @returns {Promise} containing a object with stream-names and items in the streams. + * @memberof SxcQuery + */ + getStreams(streams: string): Promise { + return this.getInternal(streams); + } + + /** + * Get all or one data entity from the backend + * @param id optional id as number or string - if not provided, will get all + * @param params optional parameters - ATM not usefuly but we plan to support more filters etc. + * @returns an array with 1 or n entities in the simple JSON format + */ + private getInternal(streams?: string, params?: string | Record): Promise { + let path = "app/auto/query/" + this.name; + if (streams && (typeof streams === 'string')) path += "?stream=" + streams; + return this.webApi.fetchJson(this.webApi.url(path, params)); + } +} diff --git a/projects/$2sxc/src/sxc-instance/sxc-instance-with-internals.ts b/projects/$2sxc/src/sxc-instance/sxc-instance-with-internals.ts index c50c58647..3a71ede92 100644 --- a/projects/$2sxc/src/sxc-instance/sxc-instance-with-internals.ts +++ b/projects/$2sxc/src/sxc-instance/sxc-instance-with-internals.ts @@ -18,6 +18,12 @@ export class SxcInstanceWithInternals extends SxcInstance { super(id, cbid, $2sxc, ctx); // Help cach error on call of old code + // Background: From v3 to v12 data had a unusualy system for retrieving data belonging to the module + // We believe it's almost never used, but the TimelineJs App always used it, and we believe + // 2-3 other examples may have as well. + // Now in v13 sxc.data is used to get any kind of data, + // and we want to make sure that old code will show a warning helping people fix this + // All the old code would have started with sxc.data.on('load', ...) so this is where we give them the error (this.data as any).on = () => { throw 'Warning Obsolete Feature on 2sxc JS: the .data has been obsolete for a long time and is repurposed. \n' + 'If you are calling .data.on(...) you are running very old code. \n' diff --git a/projects/$2sxc/src/sxc-instance/sxc-instance.ts b/projects/$2sxc/src/sxc-instance/sxc-instance.ts index c4c16f533..6f4625ae7 100644 --- a/projects/$2sxc/src/sxc-instance/sxc-instance.ts +++ b/projects/$2sxc/src/sxc-instance/sxc-instance.ts @@ -7,137 +7,150 @@ import { HasLog } from '..'; import { SxcRootInternals } from '../sxc-root/sxc-root-internals'; import { SxcInstanceManage } from './sxc-instance-manage'; import { SxcData } from './data/sxc-data'; +import { SxcQuery } from './data/sxc-query'; // const serviceScopes = ['app', 'app-sys', 'app-api', 'app-query', 'app-content', 'eav', 'view', 'dnn']; /** - * The typical sxc-instance object for a specific DNN module or content-block - */ +* The typical sxc-instance object for a specific DNN module or content-block +*/ export class SxcInstance extends HasLog implements Public.SxcInstance { + /** + * helpers for ajax calls + */ + webApi: SxcWebApi; + + /** + * The manage controller for edit/cms actions + * + * @type {*} + * @memberof SxcInstance + */ + manage: SxcInstanceManage = null; // initialize correctly later on + + constructor( + /** the sxc-instance ID, which is usually the DNN Module Id */ + public id: number, + /** + * content-block ID, which is either the module ID, or the content-block definitiion entity ID + * this is an advanced concept you usually don't care about, otherwise you should research it + */ + public cbid: number, + /** The environment information, important for http-calls */ + public readonly root: SxcRoot & SxcRootInternals, /** - * helpers for ajax calls - */ - webApi: SxcWebApi; - - /** - * The manage controller for edit/cms actions - * - * @type {*} - * @memberof SxcInstance - */ - manage: SxcInstanceManage = null; // initialize correctly later on - - constructor( - /** the sxc-instance ID, which is usually the DNN Module Id */ - public id: number, - /** - * content-block ID, which is either the module ID, or the content-block definitiion entity ID - * this is an advanced concept you usually don't care about, otherwise you should research it - */ - public cbid: number, - /** The environment information, important for http-calls */ - public readonly root: SxcRoot & SxcRootInternals, - /** - * Custom context information provided by the constructor - will replace auto-context detection - */ - public ctx?: ContextIdentifier, - ) { - super('SxcInstance', null, 'Generating for ' + id + ':' + cbid); - this.webApi = new SxcWebApi(this); - - // add manage property, but not within initializer, because inside the manage-initializer it may reference 2sxc again - try { // sometimes the manage can't be built, like before installing - if (root._manage) root._manage.initInstance(this); - } catch (e) { - console.error('error in 2sxc - will only log but not throw', e); - } - - // this only works when manage exists (not installing) and translator exists too - if (root._translateInit && this.manage) - // ensure that we really have a manage context, otherwise we can't initialize i18n and it doesn't make sense - if (this.manage.context && this.manage.context.app && this.manage.context.app.currentLanguage) - root._translateInit(this.manage); // init translate, not really nice, but ok for now - - } - - data(contentType: string) { - return new SxcData(this, contentType); - } - - /** - * converts a short api-call path like "/app/Blog/query/xyz" to the DNN full path - * which varies from installation to installation like "/desktopmodules/api/2sxc/app/..." - * @deprecated use http.apiUrl instead - * @param virtualPath - * @returns mapped path - */ - resolveServiceUrl(virtualPath: string) { - const scope = virtualPath.split('/')[0].toLowerCase(); - - // stop if it's not one of our special paths - if (ApiUrlRoots.indexOf(scope) === -1) - return virtualPath; - - return this.root.http.apiRoot(ToSxcName) + scope + '/' + virtualPath.substring(virtualPath.indexOf('/') + 1); + * Custom context information provided by the constructor - will replace auto-context detection + */ + public ctx?: ContextIdentifier, + ) { + super('SxcInstance', null, 'Generating for ' + id + ':' + cbid); + this.webApi = new SxcWebApi(this); + + // add manage property, but not within initializer, because inside the manage-initializer it may reference 2sxc again + try { // sometimes the manage can't be built, like before installing + if (root._manage) root._manage.initInstance(this); + } catch (e) { + console.error('error in 2sxc - will only log but not throw', e); } - - - // Show a nice error with more infos around 2sxc - showDetailedHttpError(result: any): any { + + // this only works when manage exists (not installing) and translator exists too + if (root._translateInit && this.manage) + // ensure that we really have a manage context, otherwise we can't initialize i18n and it doesn't make sense + if (this.manage.context && this.manage.context.app && this.manage.context.app.currentLanguage) + root._translateInit(this.manage); // init translate, not really nice, but ok for now + } + + /** + * TODO: DOCS + * + * @param {string} contentType + * @returns + * @memberof SxcInstance + */ + data(contentType: string) { + return new SxcData(this, contentType); + } + + query(query: string) { + return new SxcQuery(this, query); + } + + + /** + * converts a short api-call path like "/app/Blog/query/xyz" to the DNN full path + * which varies from installation to installation like "/desktopmodules/api/2sxc/app/..." + * @deprecated use http.apiUrl instead + * @param virtualPath + * @returns mapped path + */ + resolveServiceUrl(virtualPath: string) { + const scope = virtualPath.split('/')[0].toLowerCase(); + + // stop if it's not one of our special paths + if (ApiUrlRoots.indexOf(scope) === -1) + return virtualPath; + + return this.root.http.apiRoot(ToSxcName) + scope + '/' + virtualPath.substring(virtualPath.indexOf('/') + 1); + } + + + // Show a nice error with more infos around 2sxc + showDetailedHttpError(result: any): any { + if (window.console) + console.log(result); + + // check if the error was just because a language file couldn't be loaded - then don't show a message + if (result.status === 404 && + result.config && + result.config.url && + result.config.url.indexOf('/dist/i18n/') > -1) { if (window.console) - console.log(result); - - // check if the error was just because a language file couldn't be loaded - then don't show a message - if (result.status === 404 && - result.config && - result.config.url && - result.config.url.indexOf('/dist/i18n/') > -1) { - if (window.console) - console.log('just fyi: failed to load language resource; will have to use default'); - return result; - } - - // if it's an unspecified 0-error, it's probably not an error but a cancelled request, - // (happens when closing popups containing angularJS) - if (result.status === 0 || result.status === -1) - return result; - - // let's try to show good messages in most cases - let infoText = 'Had an error talking to the server (status ' + result.status + ').'; - const srvResp = result.responseText - ? JSON.parse(result.responseText) // for jquery ajax errors - : result.data; // for angular $http - if (srvResp) { - const msg = srvResp.Message; - if (msg) infoText += '\nMessage: ' + msg; - const msgDet = srvResp.MessageDetail || srvResp.ExceptionMessage; - if (msgDet) infoText += '\nDetail: ' + msgDet; - - - if (msgDet && msgDet.indexOf('No action was found') === 0) - if (msgDet.indexOf('that matches the name') > 0) - infoText += '\n\nTip from 2sxc: you probably got the action-name wrong in your JS.'; - else if (msgDet.indexOf('that matches the request.') > 0) - infoText += '\n\nTip from 2sxc: Seems like the parameters are the wrong amount or type.'; - - if (msg && msg.indexOf('Controller') === 0 && msg.indexOf('not found') > 0) - infoText += - // tslint:disable-next-line:max-line-length - "\n\nTip from 2sxc: you probably spelled the controller name wrong or forgot to remove the word 'controller' from the call in JS. To call a controller called 'DemoController' only use 'Demo'."; - - } - // tslint:disable-next-line:max-line-length - infoText += '\n\nif you are an advanced user you can learn more about what went wrong - discover how on 2sxc.org/help?tag=debug'; - alert(infoText); - + console.log('just fyi: failed to load language resource; will have to use default'); return result; } - - /** - * checks if we're currently in edit mode - * @returns {boolean} - */ - isEditMode(): boolean { - return (this.manage && this.manage._isEditMode()) === true; + + // if it's an unspecified 0-error, it's probably not an error but a cancelled request, + // (happens when closing popups containing angularJS) + if (result.status === 0 || result.status === -1) + return result; + + // let's try to show good messages in most cases + let infoText = 'Had an error talking to the server (status ' + result.status + ').'; + const srvResp = result.responseText + ? JSON.parse(result.responseText) // for jquery ajax errors + : result.data; // for angular $http + if (srvResp) { + const msg = srvResp.Message; + if (msg) infoText += '\nMessage: ' + msg; + const msgDet = srvResp.MessageDetail || srvResp.ExceptionMessage; + if (msgDet) infoText += '\nDetail: ' + msgDet; + + + if (msgDet && msgDet.indexOf('No action was found') === 0) + if (msgDet.indexOf('that matches the name') > 0) + infoText += '\n\nTip from 2sxc: you probably got the action-name wrong in your JS.'; + else if (msgDet.indexOf('that matches the request.') > 0) + infoText += '\n\nTip from 2sxc: Seems like the parameters are the wrong amount or type.'; + + if (msg && msg.indexOf('Controller') === 0 && msg.indexOf('not found') > 0) + infoText += + // tslint:disable-next-line:max-line-length + "\n\nTip from 2sxc: you probably spelled the controller name wrong or forgot to remove the word 'controller' from the call in JS. To call a controller called 'DemoController' only use 'Demo'."; + } + // tslint:disable-next-line:max-line-length + infoText += '\n\nif you are an advanced user you can learn more about what went wrong - discover how on 2sxc.org/help?tag=debug'; + alert(infoText); + + return result; + } + + /** + * checks if we're currently in edit mode + * @returns {boolean} + */ + isEditMode(): boolean { + return (this.manage && this.manage._isEditMode()) === true; + } } + \ No newline at end of file diff --git a/projects/$2sxc/src/sxc-instance/web-api/sxc-web-api.ts b/projects/$2sxc/src/sxc-instance/web-api/sxc-web-api.ts index da419012d..535fe0345 100644 --- a/projects/$2sxc/src/sxc-instance/web-api/sxc-web-api.ts +++ b/projects/$2sxc/src/sxc-instance/web-api/sxc-web-api.ts @@ -158,14 +158,6 @@ export class SxcWebApi implements Public.SxcWebApi { return this.fetch(url, data, method).then(response => response.json()); } - // data(typeName: string, ids?: string, params?: string | Record): Promise { - // if (typeName == null) throw 'typeName is empty'; - // if (typeName.indexOf('/') != -1 || typeName.indexOf('\\') != -1) throw 'typeName has slashes - not allowed'; - // let path = 'app/auto/content/' + typeName; - // if (ids && typeof(ids) === 'string') path += '/' + ids; - // return this.fetch(this.url(path, params)).then(response => response.json()); - // } - // // TODO: must standardize how to handle url params // // option 1: part of the query name // // - great because it would be the same signature as fetch/fetchjson