@adobe/acc-js-sdk
Advanced tools
Comparing version 1.0.9 to 1.1.0
@@ -8,2 +8,17 @@ # Adobe Campaign Classic (ACC) SDK in JavaScript (node.js and browser) | ||
## Version 1.1.0 | ||
_2022/03/05_ | ||
Changes in the metadata api (`application.getSchema`) which was not completely implemented. While this API is meant to be largely compatible with the [ACC JS API](https://docs.adobe.com/content/help/en/campaign-classic/technicalresources/api/c-Application.html), it's not always possible to do so because of the asynchronous nature of the SDK. The JS API is executed inside the Campaign application server can will synchronously and transparently fetch schemas as needed. Howerer the SDK runs outside of the Campaign server. It will synchronously and transparently fetch schemas as needed, but this will be done adynchronously. | ||
Differences are document in the `Application` section of the README. | ||
* Provide array and map access to XtkSchemaKey.fields, | ||
* The order of children of a node has been changed. Beore 1.1.0, it was attributes, then elements. After 1.1.0, it's the order defined in the schema XML | ||
* New application.getEnumeration function to retreive an enumeration | ||
* Removed the XtkSchemaNode.hasChild function | ||
* Support for ref nodes and links: XtkSchemaNode.refTarget(), XtkSchemaNode.linkTarget() functions | ||
* Reviews XtkSchemaNode.findNode() function to support links, refs, ANY type, etc. and is now asynchronous | ||
* The name attribute of enumerations (`XtkEnumeration.name`) is now the fully qualified name of the enumeration, i.e. is prefixed by the schema id | ||
## Version 1.0.9 | ||
@@ -29,3 +44,5 @@ _2022/03/02_ | ||
For breaking changes see the [migration guide](MIGRATION.md) | ||
## Version 1.0.7 | ||
@@ -32,0 +49,0 @@ _2022/01/24_ |
{ | ||
"name": "@adobe/acc-js-sdk", | ||
"version": "1.0.9", | ||
"version": "1.1.0", | ||
"description": "ACC Javascript SDK", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
@@ -25,2 +25,3 @@ /* | ||
const EntityAccessor = require('./entityAccessor.js').EntityAccessor; | ||
const { ArrayMap } = require('./util.js'); | ||
@@ -37,6 +38,2 @@ const PACKAGE_STATUS = { "never": 0, "always": 1, "default": 2, "preCreate": 3 }; | ||
// Determine if a name is an attribute name, i.e. if it starts with the "@" character | ||
const isAttributeName = function(name) { return name.length > 0 && name[0] == '@'; }; | ||
/** | ||
@@ -107,18 +104,59 @@ * Creates a schema object from an XML representation | ||
constructor(schema, xml, schemaNode) { | ||
/** | ||
* The schema this key belongs to | ||
* @type {Campaign.XtkSchema} | ||
*/ | ||
this.schema = schema; | ||
/** | ||
* The name of the key | ||
* @type {string} | ||
*/ | ||
this.name = EntityAccessor.getAttributeAsString(xml, "name"); | ||
/** | ||
* A human friendly name for they key | ||
* @type {string} | ||
*/ | ||
this.label = EntityAccessor.getAttributeAsString(xml, "label"); | ||
/** | ||
* A longer, human friendly description for they key | ||
* @type {string} | ||
*/ | ||
this.description = EntityAccessor.getAttributeAsString(xml, "desc"); | ||
/** | ||
* Indicates if the key is internal or not | ||
* @type {boolean} | ||
*/ | ||
this.isInternal = EntityAccessor.getAttributeAsBoolean(xml, "internal"); | ||
/** | ||
* Indicates if the fields (parts) of a composite key may be empty (null). At least one part must always be populated | ||
* @type {boolean} | ||
*/ | ||
this.allowEmptyPart = EntityAccessor.getAttributeAsString(xml, "allowEmptyPart"); | ||
this.fields = {}; | ||
/** | ||
* The fields making up the key | ||
* @type {Utils.ArrayMap<Campaign.XtkSchemaNode>} | ||
*/ | ||
this.fields = new ArrayMap(); | ||
for (var child of EntityAccessor.getChildElements(xml, "keyfield")) { | ||
const xpath = EntityAccessor.getAttributeAsString(child, "xpath"); | ||
if (xpath == "") throw new DomException(`Cannot create XtkSchemaKey for key '${this.name}': keyfield does not have an xpath attribute`); | ||
const field = schemaNode.findNode(xpath); | ||
this.fields[field.name] = field; | ||
const xpathString = EntityAccessor.getAttributeAsString(child, "xpath"); | ||
if (xpathString == "") throw new DomException(`Cannot create XtkSchemaKey for key '${this.name}': keyfield does not have an xpath attribute`); | ||
// find key field | ||
const xpath = new XPath(xpathString); | ||
const elements = xpath.getElements(); | ||
let keyNode = schemaNode; | ||
while (keyNode && elements.length > 0) | ||
keyNode = keyNode.children[elements.shift()]; | ||
if (keyNode) | ||
this.fields._push(xpathString, keyNode); | ||
} | ||
} | ||
} | ||
@@ -138,6 +176,17 @@ | ||
constructor(xml) { | ||
/** | ||
* The xpath of the join condition on the source table | ||
* @type {string} | ||
*/ | ||
this.src = EntityAccessor.getAttributeAsString(xml, "xpath-src"); | ||
this.dst = EntityAccessor.getAttributeAsString(xml, "xpath-dst"); | ||
/** | ||
* The xpath of the join condition on the destination table | ||
* @type {string} | ||
*/ | ||
this.dst = EntityAccessor.getAttributeAsString(xml, "xpath-dst"); | ||
} | ||
} | ||
// ======================================================================================== | ||
@@ -231,5 +280,10 @@ // Schema nodes | ||
this.img = EntityAccessor.getAttributeAsString(xml, "img"); | ||
this.image = this.img; | ||
/** | ||
* An optional image for the node (alias to the img property) | ||
* @type {string} | ||
*/ | ||
this.image = this.img; | ||
/** | ||
* Returns the name of the image of the current node in the form of a string of characters. | ||
@@ -241,3 +295,3 @@ * @type {string} | ||
/** | ||
* The node type | ||
* The node type. Attribute nodes without an explicitedly defined type will be reported as "string" | ||
* @type {string} | ||
@@ -249,3 +303,3 @@ */ | ||
/** | ||
* The node target | ||
* For link type nodes, the target of the link | ||
* @type {string} | ||
@@ -255,3 +309,3 @@ */ | ||
/** | ||
/** | ||
* The node integrity | ||
@@ -262,3 +316,3 @@ * @type {string} | ||
/** | ||
/** | ||
* The node data length (applicable for string-types only) | ||
@@ -268,2 +322,7 @@ * @type {number} | ||
this.length = EntityAccessor.getAttributeAsLong(xml, "length"); | ||
/** | ||
* The node data length (applicable for string-types only) | ||
* @type {number} | ||
*/ | ||
this.size = this.length; | ||
@@ -275,3 +334,3 @@ | ||
*/ | ||
this.enum = EntityAccessor.getAttributeAsString(xml, "enum"); | ||
this.enum = EntityAccessor.getAttributeAsString(xml, "enum"); | ||
@@ -282,3 +341,3 @@ /** | ||
*/ | ||
this.userEnumeration = EntityAccessor.getAttributeAsString(xml, "userEnum"); | ||
this.userEnumeration = EntityAccessor.getAttributeAsString(xml, "userEnum"); | ||
@@ -296,2 +355,3 @@ /** | ||
this.ref = EntityAccessor.getAttributeAsString(xml, "ref"); | ||
/** | ||
@@ -302,2 +362,7 @@ * Has an unlimited number of children of the same type | ||
this.unbound = EntityAccessor.getAttributeAsBoolean(xml, "unbound"); | ||
/** | ||
* Has an unlimited number of children of the same type | ||
* @type {boolean} | ||
*/ | ||
this.isCollection = this.unbound; | ||
@@ -310,2 +375,3 @@ | ||
this.isMappedAsXML = EntityAccessor.getAttributeAsBoolean(xml, "xml"); | ||
/** | ||
@@ -316,8 +382,9 @@ * is an advanced node | ||
this.isAdvanced = EntityAccessor.getAttributeAsBoolean(xml, "advanced"); | ||
/** | ||
* Children of the node. This is a object whose key are the names of the children nodes (without the "@" | ||
* character for attributes) | ||
* @type {Object.<string, Campaign.XtkSchemaNode>} | ||
* @type {Utils.ArrayMap.<Campaign.XtkSchemaNode>} | ||
*/ | ||
this.children = {}; | ||
this.children = new ArrayMap(); | ||
@@ -338,5 +405,5 @@ /** | ||
* Schema root elements may have a list of keys. This is a dictionary whose names are the key names and values the keys | ||
* @type {Object<string, XtkSchemaKey>} | ||
* @type {ArrayNode<Campaign.XtkSchemaKey>} | ||
*/ | ||
this.keys = {}; | ||
this.keys = new ArrayMap(); | ||
@@ -362,3 +429,3 @@ /** | ||
*/ | ||
this.isAnyType = this.type === "ANY"; | ||
this.isAnyType = this.type === "ANY"; | ||
@@ -369,3 +436,3 @@ /** | ||
*/ | ||
this.isLink = this.type === "link"; | ||
this.isLink = this.type === "link"; | ||
@@ -376,3 +443,3 @@ /** | ||
*/ | ||
this.hasEnumeration = this.enum !== ""; | ||
this.hasEnumeration = this.enum !== ""; | ||
@@ -383,3 +450,3 @@ /** | ||
*/ | ||
this.hasSQLTable = this.sqlTable !== ''; | ||
this.hasSQLTable = this.sqlTable !== ''; | ||
@@ -402,3 +469,3 @@ /** | ||
*/ | ||
this.isTemporaryTable = EntityAccessor.getAttributeAsBoolean(xml, "temporaryTable"); | ||
this.isTemporaryTable = EntityAccessor.getAttributeAsBoolean(xml, "temporaryTable"); | ||
@@ -422,3 +489,3 @@ /** | ||
*/ | ||
this.isExternalJoin = EntityAccessor.getAttributeAsBoolean(xml, "externalJoin"); | ||
this.isExternalJoin = EntityAccessor.getAttributeAsBoolean(xml, "externalJoin"); | ||
@@ -429,3 +496,3 @@ /** | ||
*/ | ||
this.isMemo = this.type === "memo" || this.type === "CDATA"; | ||
this.isMemo = this.type === "memo" || this.type === "CDATA"; | ||
@@ -436,3 +503,3 @@ /** | ||
*/ | ||
this.isMemoData = this.isMemo && this.name === 'data'; | ||
this.isMemoData = this.isMemo && this.name === 'data'; | ||
@@ -443,3 +510,3 @@ /** | ||
*/ | ||
this.isBlob = this.type === "blob"; | ||
this.isBlob = this.type === "blob"; | ||
@@ -450,4 +517,7 @@ /** | ||
*/ | ||
this.isCDATA = this.type === "CDATA"; | ||
this.isCDATA = this.type === "CDATA"; | ||
const notNull = EntityAccessor.getAttributeAsString(xml, "notNull"); | ||
const sqlDefault = EntityAccessor.getAttributeAsString(xml, "sqlDefault"); | ||
const notNullOverriden = notNull || sqlDefault === "NULL"; | ||
/** | ||
@@ -457,5 +527,2 @@ * Returns a boolean which indicates whether or not the current node can take the null value into account. | ||
*/ | ||
const notNull = EntityAccessor.getAttributeAsString(xml, "notNull"); | ||
const sqlDefault = EntityAccessor.getAttributeAsString(xml, "sqlDefault"); | ||
const notNullOverriden = notNull || sqlDefault === "NULL" | ||
this.isNotNull = notNullOverriden ? XtkCaster.asBoolean(notNull) : this.type === "int64" || this.type === "short" || | ||
@@ -475,3 +542,3 @@ this.type === "long" || this.type === "byte" || this.type === "float" || this.type === "double" || | ||
*/ | ||
this.isSQL = !!this.SQLName || !!this.SQLTable || (this.isLink && this.schema.mappingType === 'sql' && !this.isMappedAsXML); | ||
this.isSQL = !!this.SQLName || !!this.SQLTable || (this.isLink && this.schema.mappingType === 'sql' && !this.isMappedAsXML); | ||
@@ -494,3 +561,3 @@ /** | ||
*/ | ||
this.isCalculated = false; | ||
this.isCalculated = false; | ||
@@ -501,3 +568,3 @@ /** | ||
*/ | ||
this.expr = EntityAccessor.getAttributeAsString(xml, "expr"); | ||
this.expr = EntityAccessor.getAttributeAsString(xml, "expr"); | ||
if (this.expr) this.isCalculated = true; | ||
@@ -509,3 +576,3 @@ | ||
*/ | ||
this.isAutoIncrement = EntityAccessor.getAttributeAsBoolean(xml, "autoIncrement"); | ||
this.isAutoIncrement = EntityAccessor.getAttributeAsBoolean(xml, "autoIncrement"); | ||
@@ -544,18 +611,20 @@ /** | ||
const childNodes = []; | ||
for (const child of EntityAccessor.getChildElements(xml, "attribute")) { | ||
const node = new XtkSchemaNode(); | ||
node.init(schema, child, this, true); | ||
childNodes.push(node); | ||
for (const child of EntityAccessor.getChildElements(xml)) { | ||
if (child.tagName === "attribute") { | ||
const node = new XtkSchemaNode(); | ||
node.init(schema, child, this, true); | ||
childNodes.push(node); | ||
} | ||
if (child.tagName === "element") { | ||
const node = new XtkSchemaNode(); | ||
node.init(schema, child, this, false); | ||
childNodes.push(node); | ||
} | ||
if (child.tagName === "compute-string") { | ||
this.expr = EntityAccessor.getAttributeAsString(child, "expr"); | ||
this.isCalculated = false; | ||
} | ||
} | ||
for (const child of EntityAccessor.getChildElements(xml, "element")) { | ||
const node = new XtkSchemaNode(); | ||
node.init(schema, child, this, false); | ||
childNodes.push(node); | ||
} | ||
for (const childNode of childNodes) { | ||
if (this.children[childNode.name]) { | ||
// already a child with the name => there's a problem with the schema | ||
throw new DomException(`Failed to create schema node '${childNode.name}': there's a already a node with this name`); | ||
} | ||
this.children[childNode.name] = childNode; | ||
this.children._push(childNode.name, childNode); | ||
this.childrenCount = this.childrenCount + 1; | ||
@@ -567,3 +636,3 @@ } | ||
const key = new XtkSchemaKey(schema, child, this); | ||
this.keys[key.name] = key; | ||
this.keys._push(key.name, key); | ||
} | ||
@@ -577,17 +646,2 @@ | ||
/** | ||
* Does the node have a child with the given name? | ||
* | ||
* @param {string} name the child name, without the "@" character for attributes | ||
* @returns {boolean} a boolean indicating whether the node contains a child with the given name | ||
*/ | ||
hasChild(name) { | ||
var child = this.children[name]; | ||
if (child) return true; | ||
// TODO: handle ref target | ||
// if (this.hasRefTarget()) | ||
// return this.refTarget().hasChild(name); | ||
return false; | ||
} | ||
/** | ||
* Indicates whether the current node has an unlimited number of children of the same type. | ||
@@ -629,17 +683,63 @@ * | ||
/** | ||
* Find the target of a ref node. | ||
* @returns {Promise<Campaign.XtkNode>} the target node, or undefined if not found | ||
*/ | ||
async refTarget() { | ||
if (!this.ref) return; | ||
const index = this.ref.lastIndexOf(':'); | ||
if (index !== -1) { | ||
// find the associated schame | ||
const refSchemaId = this.ref.substring(0, index); | ||
if (refSchemaId.indexOf(':') === -1) | ||
throw Error(`Cannot find ref target '${this.ref}' from node '${this.nodePath}' of schema '${this.schema.id}': ref value is not correct (expeted <schemaId>:<path>)`); | ||
const refPath = this.ref.substring(index + 1); | ||
// inside current schema ? | ||
if (refSchemaId === this.schema.id) | ||
return this.schema.findNode(refPath); | ||
const refSchema = await this.schema._application.getSchema(refSchemaId); | ||
if (!refSchema) return; | ||
return refSchema.findNode(refPath); | ||
} | ||
else { | ||
// ref is in the current schema | ||
return this.schema.findNode(this.ref); | ||
} | ||
} | ||
/** | ||
* Returns an instance of XtkSchemaNode or null if the node doesn't exist and the mustExist parameter is set to false. | ||
* Find the target of a link node. | ||
* @returns {Promise<Campaign.XtkNode>} the target node, or undefined if not found | ||
*/ | ||
async linkTarget() { | ||
if (this.type !== "link") return this.schema.root; | ||
let schemaId = this.target; | ||
let xpath = ""; | ||
if (this.target.indexOf(',') !== -1) | ||
throw new Error(`Cannot find target of link '${this.target}': target has multiple schemas`); | ||
const index = this.target.indexOf('/'); | ||
if (index !== -1) { | ||
xpath = this.target.substring(index + 1); | ||
schemaId = this.target.substring(0, index); | ||
xpath = this.target.substring(index + 1); | ||
} | ||
if (schemaId.indexOf(':') === -1) | ||
throw new Error(`Cannot find target of link '${this.target}': target is not a valid link target (missing schema id)`); | ||
const schema = await this.schema._application.getSchema(schemaId); | ||
if (!schema) return; | ||
const root = schema.root; | ||
if (!root) return; | ||
if (!xpath) return root; | ||
return await root.findNode(xpath); | ||
} | ||
/** | ||
* Returns an instance of XtkSchemaNode or null if the node doesn't exist. In version 1.1.0 and above, this function is | ||
* asynchronous (returns a Promise) | ||
* | ||
* @param {XML.XPath|string} path XPath represents the name of the node to be searched | ||
* @param {boolean} strict indicates whether (strict to false) or not, when the name of the last item in the path does not exist as is, it should be searched for as an attribute or an element. By default to true. | ||
* @param {boolean} mustExist indicates whether an exception must be raised if the node does not exist. true by default | ||
* @returns Returns a XtkSchemaNode instance if the node can be found, or null if the mustExist parameter is set to false. | ||
* @throws {Error} if the request cannot be find (when mustExist is set) | ||
* @returns {Promise<XtkSchemaNode>} Returns a XtkSchemaNode instance if the node can be found | ||
*/ | ||
findNode(path, strict, mustExist) { | ||
if (strict === undefined) strict = true; | ||
if (mustExist === undefined) mustExist = true; | ||
if (typeof path == "string") | ||
path = new XPath(path); | ||
async findNode(path) { | ||
if (typeof path == "string") path = new XPath(path); | ||
@@ -650,4 +750,3 @@ // Find the starting node | ||
node = this.schema.root; | ||
if (!node) | ||
throw new DomException(`Cannot find node '${path}' in node ${this.name} : schema ${this.schema.name} does not have a root node`); | ||
if (!node) return; | ||
path = path.getRelativePath(); | ||
@@ -657,4 +756,3 @@ } | ||
// Special case for current path "." | ||
if (path.isSelf()) | ||
return this; | ||
if (path.isSelf()) return this; | ||
@@ -667,24 +765,18 @@ const elements = path.getElements(); | ||
// TODO: if the path is a collection path, ignore the collection index | ||
// TODO: handle ref elements (consider the ref target instead) | ||
// TODO: Handle link between schemas | ||
// TODO: Handle any type | ||
if (!strict && elements.length == 0 && (!node.children[name] || !isAttributeName(name))) { | ||
// name is the final part of the path and the associated definition | ||
// does not exists. Since strict is set to false we check if the | ||
// alternate name exists (element name for an attribute or attribute | ||
// name for an element). | ||
var found = node.children[name]; | ||
if (!found && isAttributeName(name)) found = node.children[name.substring(1)]; | ||
if (!found && !isAttributeName(name)) found = node.children[`@${name}`]; | ||
if (found) name = found.name; | ||
} | ||
// handle ref elements (consider the ref target instead) | ||
if (node.ref) node = await node.refTarget(); | ||
if (!node) break; | ||
if (node.type === "link") node = await node.linkTarget(); | ||
if (!node) break; | ||
// Don't continue for any type | ||
// kludge to accept immediate child of an ANY type node (cas in packages) | ||
if (node.type === 'ANY') return this.children[name]; | ||
var childNode = null; | ||
if (element.isSelf()) | ||
childNode = node; | ||
else if (element.isParent()) | ||
childNode = node.parent; | ||
else | ||
childNode = node._getChildDefAutoExpand(name, mustExist); | ||
if (element.isSelf()) childNode = node; | ||
else if (element.isParent()) childNode = node.parent; | ||
else childNode = await node.children[name]; | ||
node = childNode; | ||
@@ -695,26 +787,2 @@ } | ||
// See CXtkNodeDef::GetChildDefAutoExpand | ||
_getChildDefAutoExpand(name, mustExist) { | ||
var child = this.children[name]; | ||
if (child) | ||
return child; | ||
// TODO: handle ref | ||
if (mustExist) { | ||
// TODO: handle auto-expand schemas | ||
const path = this._getNodePath(); | ||
const isAttribute = isAttributeName(name); | ||
const schemaDesc = this.schema.userDescription; | ||
if( path.isRootPath() ) { | ||
if (isAttribute) throw new DomException(`Unknown attribute '${name.substring(1)}' (see definition of schema '${schemaDesc}').`); | ||
else throw new DomException(`Unknown element '${name}' (see definition of schema '${schemaDesc}').`); | ||
} | ||
if (isAttribute) throw new DomException(`Unknown attribute '${name.substring(1)}' (see definition of element '${path.asString()}' in schema '${schemaDesc}').`); | ||
else throw new DomException(`Unknown element '${name}' (see definition of element '${path.asString()}' in schema '${schemaDesc}').`); | ||
} | ||
return null; | ||
} | ||
/** | ||
@@ -730,4 +798,4 @@ * Internal recursive function used to create a multi-line debug string representing the schema | ||
var s = `${indent}${this.label} (${this.name})\n`; | ||
for (var name in this.children) { | ||
s = s + this.children[name].toString(` ${indent}`); | ||
for (var child of this.children) { | ||
s = s + child.toString(` ${indent}`); | ||
} | ||
@@ -737,2 +805,93 @@ return s; | ||
/** | ||
* Return the XtkSchemaNodes making up the join of a link-type node | ||
* @returns {Promise<Array>} returns an array of joins. Each join is an element having a source and destination attributes, whose value is the corresponding XtkSchemaNode | ||
*/ | ||
async joinNodes() { | ||
if (!this.isLink) return; | ||
const joinParts = []; | ||
for (const join of this.joins) { | ||
const source = await this.parent.findNode(join.src); | ||
let destination = await this.linkTarget(); | ||
if (destination) | ||
destination = await destination.findNode(join.dst); | ||
if (source && destination) | ||
joinParts.push({ | ||
source: source, | ||
destination: destination | ||
}); | ||
} | ||
return joinParts; | ||
} | ||
/** | ||
* Returns the reverse link node of a link-type node | ||
* @returns {Promise<Campaign.XtkSchemaNode>} | ||
*/ | ||
async reverseLink() { | ||
if (!this.isLink) return; | ||
const target = await this.linkTarget(); | ||
if (!target) return; | ||
const revLink = await target.findNode(this.revLink); | ||
return revLink; | ||
} | ||
/** | ||
* Returns the compute string of a node. As the node can be a link or a reference, this function is asynchronous | ||
* @returns {Promise<string>} | ||
*/ | ||
async computeString() { | ||
if (this.expr) return this.expr; | ||
// if we are a ref: ask the target of the ref | ||
if (this.ref) { | ||
const refTarget = await this.refTarget(); | ||
if (!refTarget) return ""; | ||
return await refTarget.computeString(); | ||
} | ||
// No compute-string found: generate a default one (first key field) | ||
if (this.keys && this.keys.length > 0) { | ||
const key = this.keys[0]; | ||
if (key && key.fields && key.fields.length > 0 && key.fields[0]) | ||
return this.schema._application.client.sdk.expandXPath(key.fields[0].nodePath); | ||
} | ||
return ""; | ||
} | ||
/** | ||
* Returns an Enumeration object which is the enumeration linked to the current node or null if there is no enumeration. | ||
* @param {string} an optional enumeration name. If none is specified, the node `enum` property will be used | ||
* @returns Promise<Campaign.XtkEnumeration> | ||
*/ | ||
async enumeration(optionalName) { | ||
const name = optionalName || this.enum; | ||
if (!name) return; | ||
const enumaration = await this.schema._application.getSysEnum(name, this.schema); | ||
return enumaration; | ||
} | ||
/** | ||
* Get the first internal key (if there is one) | ||
* @returns {Campaign.XtkSchemaKey} | ||
*/ | ||
firstInternalKeyDef() { | ||
return this.keys.find((k) => k.isInternal); | ||
} | ||
/** | ||
* Get the first external key (if there is one) | ||
* @returns {Campaign.XtkSchemaKey} | ||
*/ | ||
firstExternalKeyDef() { | ||
return this.keys.find((k) => !k.isInternal); | ||
} | ||
/** | ||
* Get the first key (internal first) | ||
* @returns {Campaign.XtkSchemaKey} | ||
*/ | ||
firstKeyDef() { | ||
let key = this.firstInternalKeyDef(); | ||
if (!key) key = this.firstExternalKeyDef(); | ||
return key; | ||
} | ||
} | ||
@@ -816,3 +975,3 @@ | ||
/** | ||
* The system enumeration name | ||
* The system enumeration name, fully qualified, i.e. prefixed with the schema id | ||
* @type {string} | ||
@@ -827,2 +986,3 @@ */ | ||
this.label = EntityAccessor.getAttributeAsString(xml, "label"); | ||
/** | ||
@@ -833,7 +993,9 @@ * A human friendly long description of the enumeration | ||
this.description = EntityAccessor.getAttributeAsString(xml, "desc"); | ||
/** | ||
* The type of the enumeration | ||
* The type of the enumeration, usually "string" or "byte" | ||
* @type {Campaign.XtkEnumerationType} | ||
*/ | ||
this.baseType = EntityAccessor.getAttributeAsString(xml, "basetype"); | ||
/** | ||
@@ -844,2 +1006,3 @@ * The default value of the enumeration | ||
this.default = null; | ||
/** | ||
@@ -850,7 +1013,8 @@ * Indicates if the enumeration has an image, i.e. if any of its values has an image | ||
this.hasImage = false; | ||
/** | ||
* The enumerations values | ||
* @type {Object<string, Campaign.XtkEnumerationValue>} | ||
* @type {Utils.ArrayMap<Campaign.XtkEnumerationValue>} | ||
*/ | ||
this.values = {}; | ||
this.values = new ArrayMap(); | ||
@@ -861,3 +1025,3 @@ var defaultValue = EntityAccessor.getAttributeAsString(xml, "default"); | ||
const e = new XtkEnumerationValue(child, this.baseType); | ||
this.values[e.name] = e; | ||
this.values._push(e.name, e); | ||
if (e.image != "") this.hasImage = true; | ||
@@ -870,2 +1034,9 @@ const stringValue = EntityAccessor.getAttributeAsString(child, "value"); | ||
propagateImplicitValues(this, true); | ||
/** | ||
* The system enumeration name, without the schema id prefix | ||
* @type {string} | ||
*/ | ||
this.shortName = this.name; | ||
this.name = `${schemaId}:${this.shortName}`; | ||
} | ||
@@ -899,2 +1070,3 @@ } | ||
this.namespace = EntityAccessor.getAttributeAsString(xml, "namespace"); | ||
/** | ||
@@ -906,2 +1078,3 @@ * The schema id, in the form "namespace:name" | ||
this.id = `${this.namespace}:${this.name}`; | ||
/** | ||
@@ -912,2 +1085,3 @@ * Indicates whether the schema is a library schema or not | ||
this.isLibrary = EntityAccessor.getAttributeAsBoolean(xml, "library"); | ||
/** | ||
@@ -918,2 +1092,3 @@ * A human name for the schema, in singular | ||
this.labelSingular = EntityAccessor.getAttributeAsString(xml, "labelSingular"); | ||
/** | ||
@@ -924,7 +1099,9 @@ * The schema mappgin type, following the xtk:srcSchema:mappingType enumeration | ||
this.mappingType = EntityAccessor.getAttributeAsString(xml, "mappingType"); | ||
/** | ||
* The MD5 code of the schema in the form of a hexadecimal string | ||
* The MD5 checksum of the schema in the form of a hexadecimal string | ||
* @type {string} | ||
*/ | ||
this.md5 = EntityAccessor.getAttributeAsString(xml, "md5"); | ||
/** | ||
@@ -954,9 +1131,8 @@ * The schema definition | ||
* corresponding enumeration definitions | ||
* @type {Object<string, XtkEnumeration>} | ||
* @type {Utils.ArrayMap<Campaign.XtkEnumeration>} | ||
*/ | ||
this.enumerations = {}; | ||
this.enumerations = new ArrayMap(); | ||
for (var child of EntityAccessor.getChildElements(xml, "enumeration")) { | ||
const e = new XtkEnumeration(this.id, child); | ||
this.enumerations[e.name] = e; | ||
this.enumerations._push(e.shortName, e); | ||
} | ||
@@ -972,4 +1148,4 @@ } | ||
var s = `${this.userDescription}\n`; | ||
for (var name in this.children) { | ||
s = s + this.children[name].toString(" - "); | ||
for (var child of this.children) { | ||
s = s + child.toString(" - "); | ||
} | ||
@@ -1004,2 +1180,3 @@ return s; | ||
this.login = EntityAccessor.getAttributeAsString(userInfo, "login"); | ||
/** | ||
@@ -1010,2 +1187,3 @@ * The operator login id | ||
this.id = EntityAccessor.getAttributeAsLong(userInfo, "loginId"); | ||
/** | ||
@@ -1016,2 +1194,3 @@ * A human friendly string naming the operator (compute string) | ||
this.computeString = EntityAccessor.getAttributeAsString(userInfo, "loginCS"); | ||
/** | ||
@@ -1022,2 +1201,3 @@ * The operator timezone | ||
this.timezone = EntityAccessor.getAttributeAsString(userInfo, "timezone"); | ||
/** | ||
@@ -1144,2 +1324,28 @@ * The llist of operator rights | ||
} | ||
/** | ||
* Get a system enumeration | ||
* | ||
* @param {string} enumerationName The name of the enumeration, which can be fully qualified (ex: "nms:recipient:gender") or not (ex: "gender") | ||
* @param {string} schemaOrSchemaId An optional schema id. If the enumerationName is not qualified, the search for the enumeration will be done in this schema | ||
* @returns {XtkEnumeration} the enumeration | ||
*/ | ||
async getSysEnum(enumerationName, schemaOrSchemaId) { | ||
const index = enumerationName.lastIndexOf(':'); | ||
if (index === -1) { | ||
let schema = schemaOrSchemaId; | ||
if (schema && typeof schema === "string") | ||
schema = await this.getSchema(schema); | ||
// unqualified enumeration name | ||
if (!schema) return; | ||
return schema.enumerations[enumerationName]; | ||
} | ||
// qualified enumeration name | ||
const schemaId = enumerationName.substring(0, index); | ||
if (schemaId.indexOf(':') === -1) | ||
throw Error(`Invalid enumeration name '${enumerationName}': expecting {name} or {schemaId}:{name}`); | ||
let schema = await this.getSchema(schemaId); | ||
if (!schema) return; | ||
return schema.enumerations[enumerationName.substring(index + 1)]; | ||
} | ||
} | ||
@@ -1146,0 +1352,0 @@ |
@@ -194,3 +194,3 @@ /* | ||
* @param {string} type the xtk type | ||
* @returns | ||
* @returns {string} the text literal which can be used in a Xtk expression or condition | ||
*/ | ||
@@ -197,0 +197,0 @@ xtkConstText(value, type) { |
144
src/util.js
@@ -138,5 +138,149 @@ /* | ||
/** | ||
* The ArrayMap object is used to access elements as either an array or a map | ||
* | ||
* @class | ||
* @constructor | ||
* @memberof Utils | ||
*/ | ||
class ArrayMap { | ||
constructor() { | ||
// List of items, as an ordered array. Use defineProperty to make it non-enumerable | ||
// and support for for ... in loop to iterate by item key | ||
Object.defineProperty(this, "_items", { | ||
value: [], | ||
writable: false, | ||
enumerable: false, | ||
}); | ||
Object.defineProperty(this, "_map", { | ||
value: [], | ||
writable: false, | ||
enumerable: false, | ||
}); | ||
// Number of items. Use defineProperty to make it non-enumerable | ||
// and support for for ... in loop to iterate by item key | ||
Object.defineProperty(this, "length", { | ||
value: 0, | ||
writable: true, | ||
enumerable: false, | ||
}); | ||
} | ||
_push(key, value) { | ||
let isNumKey = false; | ||
if (key) { | ||
// reserved keyworkds | ||
const isReserved = key === "_items" || key === "length" || key === "_push" || key === "forEach" || key === "map" || key === "_map" || key === "get" || key === "find" || key === "flatMap" || key === "filter"; | ||
// already a child with the name => there's a problem with the schema | ||
if (!isReserved && this[key]) throw new Error(`Failed to add element '${key}' to ArrayMap. There's already an item with the same name`); | ||
// Set key as a enumerable property, so that elements can be accessed by key, | ||
// but also iterated on with a for ... in loop | ||
// For compatibility | ||
if (!isReserved) this[key] = value; | ||
this._map[key] = value; | ||
// Special case where keys are numbers or strings convertible with numbers | ||
const numKey = +key; | ||
if (numKey === numKey) { | ||
// keys is a number. If it matches the current index, then we are good, | ||
// and we can add the property as an enumerable property | ||
isNumKey = true; | ||
} | ||
} | ||
if (!isNumKey) { | ||
// Set the index property so that items can be accessed by array index. | ||
// However, make it non-enumerable to make sure indexes do not show up in a for .. in loop | ||
Object.defineProperty(this, this._items.length, { | ||
value: value, | ||
writable: false, | ||
enumerable: false, | ||
}); | ||
} | ||
// Add to array and set length | ||
this._items.push(value); | ||
this.length = this._items.length; | ||
} | ||
/** | ||
* Executes a provided function once for each array element. | ||
* @param {*} callback Function that is called for every element of the array | ||
* @param {*} thisArg Optional value to use as this when executing the callback function. | ||
* @returns a new array | ||
*/ | ||
forEach(callback, thisArg) { | ||
return this._items.forEach(callback, thisArg); | ||
} | ||
/** | ||
* Returns the first element that satisfies the provided testing function. If no values satisfy the testing function, undefined is returned. | ||
* @param {*} callback Function that is called for every element of the array | ||
* @param {*} thisArg Optional value to use as this when executing the callback function. | ||
* @returns the first element matching the testing function | ||
*/ | ||
find(callback, thisArg) { | ||
return this._items.find(callback, thisArg); | ||
} | ||
/** | ||
* creates a new array with all elements that pass the test implemented by the provided function. | ||
* @param {*} callback Function that is called for every element of the array | ||
* @param {*} thisArg Optional value to use as this when executing the callback function. | ||
* @returns an array containing elements passing the test function | ||
*/ | ||
filter(callback, thisArg) { | ||
return this._items.filter(callback, thisArg); | ||
} | ||
/** | ||
* Get a element by either name (access as a map) or index (access as an array). Returns undefined if the element does not exist or | ||
* if the array index is out of range. | ||
* @param {string|number} indexOrKey the name or index of the element | ||
* @returns the element matching the name or index | ||
*/ | ||
get(indexOrKey) { | ||
if (typeof indexOrKey === 'number') return this._items[indexOrKey]; | ||
return this._map[indexOrKey]; | ||
} | ||
/** | ||
* Creates a new array populated with the results of calling a provided function on every element in the calling array. | ||
* @param {*} callback Function that is called for every element of the array | ||
* @param {*} thisArg Optional value to use as this when executing the callback function. | ||
* @returns a new array | ||
*/ | ||
map(callback, thisArg) { | ||
return this._items.map(callback, thisArg); | ||
} | ||
/** | ||
* Returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level. | ||
* @param {*} callback Function that is called for every element of the array | ||
* @param {*} thisArg Optional value to use as this when executing the callback function. | ||
* @returns a new array | ||
*/ | ||
flatMap(callback, thisArg) { | ||
return this._items.flatMap(callback, thisArg); | ||
} | ||
/** | ||
* Iterates over all the elements using the for ... of syntax. | ||
* @returns returns each element one after the other | ||
*/ | ||
*[Symbol.iterator] () { | ||
for (const item of this._items) { | ||
yield item; | ||
} | ||
} | ||
} | ||
// Public expots | ||
exports.Util = Util; | ||
exports.ArrayMap = ArrayMap; | ||
})(); |
@@ -434,2 +434,7 @@ /* | ||
/** | ||
* Tests if a given type is a date or time type | ||
* @param {string|number} type the type name | ||
* @returns {boolean} true if the type is a date and/or time type | ||
*/ | ||
static isTimeType(type) { | ||
@@ -439,7 +444,17 @@ return type === "datetime" || type === "datetimetz" || type === "datetimenotz" || type === "timestamp" || type === "date" || type === "time" || type === "timespan" || type === 7 || type === 10 || type === 14; | ||
static isStringType(type) { | ||
/** | ||
* Tests if a given type is a string type | ||
* @param {string|number} type the type name | ||
* @returns {boolean} true if the type is a string type | ||
*/ | ||
static isStringType(type) { | ||
return type === "string" || type === "memo" || type === 6 || type === 12 || type === 13 || type === "blob" || type === "html" || type === "CDATA"; | ||
} | ||
static isNumericType(type) { | ||
/** | ||
* Tests if a given type is a numeric type | ||
* @param {string|number} type the type name | ||
* @returns {boolean} true if the type is a numeric type | ||
*/ | ||
static isNumericType(type) { | ||
return type === "byte" || type === 1 || type === "short" || type === 2 || type === "int" || type === "long" || type === 3 || type === "float" || type === 4 || type === "double" || type === 5 || type === "timespan" || type === 14; | ||
@@ -446,0 +461,0 @@ } |
@@ -20,3 +20,3 @@ /* | ||
const { Util } = require('../src/util.js'); | ||
const { Util, ArrayMap } = require('../src/util.js'); | ||
const { SafeStorage, Cache } = require('../src/cache.js'); | ||
@@ -236,2 +236,168 @@ | ||
describe("ArrayMap", () => { | ||
it("Should support access by keys", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
am._push("world", "World"); | ||
expect(am["hello"]).toBe("Hello"); | ||
expect(am["world"]).toBe("World"); | ||
expect(am.get("hello")).toBe("Hello"); | ||
expect(am.get("world")).toBe("World"); | ||
}); | ||
it("Should support access by index", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
am._push("world", "World"); | ||
expect(am[0]).toBe("Hello"); | ||
expect(am[1]).toBe("World"); | ||
expect(am.get(0)).toBe("Hello"); | ||
expect(am.get(1)).toBe("World"); | ||
}); | ||
it("Should support length attribute", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
am._push("world", "World"); | ||
expect(am.length).toBe(2); | ||
}); | ||
it("Should support iterators (for...of)", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
am._push("world", "World"); | ||
let cat = ""; | ||
for (const s of am) cat = cat + s; | ||
expect(cat).toBe("HelloWorld"); | ||
}); | ||
it("Should support map()", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
am._push("world", "World"); | ||
const cat = am.map(s => s).join(','); | ||
expect(cat).toBe("Hello,World"); | ||
}); | ||
it("Should support flatMap()", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
am._push("world", ["Adobe", "World"]); | ||
const cat = am.flatMap(s => s).join(','); | ||
expect(cat).toBe("Hello,Adobe,World"); | ||
}); | ||
it("Should support find()", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
am._push("world", "World"); | ||
const world = am.find(s => s === 'World'); | ||
expect(world).toBe("World"); | ||
const notFound = am.find(s => s === 'NotFound'); | ||
expect(notFound).toBe(undefined); | ||
}); | ||
it("Should support filter()", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
am._push("world", "World"); | ||
const all = am.filter(s => true); | ||
expect(all).toMatchObject([ "Hello", "World" ]); | ||
const none = am.filter(s => false); | ||
expect(none).toMatchObject([ ]); | ||
const world = am.filter(s => s === 'World'); | ||
expect(world).toMatchObject([ "World" ]); | ||
}); | ||
it("Should support forEach", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
am._push("world", "World"); | ||
let cat = ""; | ||
am.forEach(s => cat = cat + s); | ||
expect(cat).toBe("HelloWorld"); | ||
}); | ||
it("Should support forEach as a key", () => { | ||
const am = new ArrayMap(); | ||
am._push("forEach", "Hello"); | ||
const cat = am.map(s => s).join(','); | ||
expect(cat).toBe("Hello"); | ||
expect(typeof am.forEach).toBe('function'); | ||
expect(am["forEach"]).not.toBe("Hello"); // forEach is a function | ||
expect(am.get("forEach")).toBe("Hello"); | ||
}); | ||
it("Should support for...in", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
am._push("world", "World"); | ||
let cat = ""; | ||
for (const s in am) cat = cat + s; | ||
expect(cat).toBe("helloworld"); | ||
}); | ||
it("Should not support for...in when there's a property named 'forEach'", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
am._push("forEach", "World"); | ||
let cat = ""; | ||
for (const s in am) cat = cat + s; | ||
expect(cat).toBe("hello"); | ||
}); | ||
it("Should support enumerations whose key is a number", () => { | ||
// For instance the "addressQuality" enumeration | ||
const am = new ArrayMap(); | ||
am._push("0", { name:"0", value:0 }); | ||
am._push("1", { name:"1", value:1 }); | ||
am._push("2", { name:"2", value:2 }); | ||
let cat = ""; | ||
for (const k in am) cat = cat + am.get(k).name; | ||
expect(cat).toBe("012"); | ||
}); | ||
it("Should not support adding the same key twice", () => { | ||
const am = new ArrayMap(); | ||
am._push("hello", "Hello"); | ||
expect(() => { am._push("hello", "World"); }).toThrow("Failed to add element 'hello' to ArrayMap. There's already an item with the same name"); | ||
}); | ||
it("Should support missing names", () => { | ||
const am = new ArrayMap(); | ||
am._push("", { name:"0", value:0 }); | ||
am._push(undefined, { name:"1", value:1 }); | ||
am._push(null, { name:"2", value:2 }); | ||
expect(am.length).toBe(3); | ||
expect(am[0].name).toBe("0"); | ||
expect(am[1].name).toBe("1"); | ||
expect(am[2].name).toBe("2"); | ||
}); | ||
it("Should handle compatibility", () => { | ||
const am = new ArrayMap(); | ||
am._push("perfect", { name:"perfect", value:0 }); | ||
am._push("notPerfect", { name:"notPerfect", value:1 }); | ||
am._push("error", { name:"error", value:2 }); | ||
// length | ||
expect(am.length).toBe(3); | ||
// Access by name | ||
expect(am.perfect).toMatchObject({ name:"perfect", value:0 }); | ||
expect(am.notPerfect).toMatchObject({ name:"notPerfect", value:1 }); | ||
expect(am.error).toMatchObject({ name:"error", value:2 }); | ||
expect(am.notFound).toBeUndefined(); | ||
// for .. in loop | ||
const list = []; | ||
for (const p in am) list.push(p); | ||
expect(list).toMatchObject([ "perfect", "notPerfect", "error" ]); | ||
}); | ||
}); | ||
describe("Is Browser", () => { | ||
it("Should not be a browser", () => { | ||
expect(Util.isBrowser()).toBe(false); | ||
}); | ||
}); | ||
}); | ||
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1273538
61
24477
1828