├── .gitignore ├── .screwdriver └── project.json ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── README.md ├── index.js ├── package.json ├── src ├── graph.js ├── graph_index.js ├── property.js └── schema.js └── tests ├── index.js ├── run.js └── schema_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | config.json 3 | log/* 4 | snippets/* 5 | doc 6 | cov 7 | pkg 8 | .DS_Store 9 | coverage.html 10 | coverage/* 11 | *.db 12 | \#* 13 | _Yardoc 14 | .yardoc 15 | tmp/* 16 | README.bak 17 | node_modules 18 | /lib/chronicle 19 | /lib/util 20 | -------------------------------------------------------------------------------- /.screwdriver/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": [ 3 | { 4 | "repository": "https://github.com/substance/util.git", 5 | "folder": "node_modules/substance-util", 6 | "branch": "lens" 7 | }, 8 | { 9 | "repository": "https://github.com/substance/data.git", 10 | "folder": ".", 11 | "branch": "lens" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.8.0 2 | ============= 3 | * General improvements 4 | * API stabilizations 5 | * Tests 6 | 7 | 0.7.0 8 | ============= 9 | * Switched to operations for graph manipulation 10 | * Persistence through Substance.Store 11 | * Graphs can now be versioned through Substance.Chronicle 12 | 13 | 0.6.0 14 | ============= 15 | * Native data structures (Arrays and Objects instead of Data.Hash datastructures) 16 | * Dropped backends (this will be reimplemented) 17 | 18 | 0.4.0 19 | ============= 20 | * Data.Graph#set now takes just one parameter, the id is specified as _id and optional 21 | 22 | 0.3.1 23 | ============= 24 | * Added Data.Hash#range 25 | * Fixed an issue with Data.Graph#merge 26 | 27 | 0.3.0 28 | ============= 29 | * Listen for node updates in realtime 30 | * Data.Graph#find now accepts an array of queries 31 | * Index queries against CouchDB 32 | * Completely rewritten CouchAdapter 33 | 34 | 0.2.2 35 | ============= 36 | 37 | * Updated Data.Graph#fetch 38 | - options are now optional 39 | - returns a Data.Hash of fetched objects 40 | * Added Data.Hash#difference 41 | * Added Data.Hash#rest 42 | * Data.Graph#merge is now chainable 43 | 44 | 0.2.1 45 | ============= 46 | 47 | * Added Data.Collection#find 48 | * Added Data.Collection#filter 49 | * Added ALL-OF operator &= 50 | * ANY-OF operator now also works with unique properties -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Michael Aufreiter, Oliver Buchtala 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Substance.Data 2 | ===================== 3 | 4 | **No longer maintained. Use http://github.com/substance/substance instead.** 5 | 6 | Substance.Data is a data representation framework for Javascript. It's being developed in the context of Substance, an open publishing platform. 7 | 8 | 9 | With Substance.Data you can: 10 | --------------------- 11 | 12 | * Model your domain data using a simple graph-based object model that can be serialized to JSON. 13 | * Traverse your graph, including relationships using a simple API. 14 | * Manipulate and query data on the client (browser) or on the server (Node.js) by using exactly the same API. 15 | 16 | Features 17 | --------------------- 18 | 19 | * `Data.Graph` (A data abstraction for all kinds of linked data) 20 | * [Persistence](http://github.com/substance/store) (You can persist your data to a Data.Store) 21 | * [Operational Transformation](http://github.com/substance/operator) (for incremental updates) 22 | * [Versioning](http://github.com/substance/chronicle) (Every graph operation is remembered and can be reverted) 23 | 24 | Install 25 | --------------------- 26 | 27 | Using the latest NPM release 28 | 29 | ```bash 30 | $ npm install substance-data 31 | ``` 32 | 33 | Or clone from repository 34 | 35 | ```bash 36 | $ git clone https://github.com/substance/data 37 | $ cd data 38 | $ npm install 39 | ``` 40 | 41 | For running the testsuite, make sure you have mocha installed 42 | 43 | ```bash 44 | sudo npm install -g mocha 45 | ``` 46 | 47 | Run the tests 48 | 49 | ```bash 50 | npm test 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Data = {}; 4 | 5 | // Current version of the library. Keep in sync with `package.json`. 6 | Data.VERSION = '0.8.0'; 7 | 8 | Data.Graph = require('./src/graph'); 9 | 10 | module.exports = Data; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "substance-data", 3 | "description": "A Javascript data representation library.", 4 | "url": "http://github.com/substance/data/", 5 | "keywords": [ 6 | "data", 7 | "linked-data", 8 | "graph" 9 | ], 10 | "author": "Michael Aufreiter", 11 | "contributors": [ 12 | "Oliver Buchtala" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/substance/data.git" 17 | }, 18 | "dependencies": { 19 | "underscore": "1.5.x", 20 | "substance-util": "substance/util.git#97dabc022c28a01575c207ce986b5ffa36361bb6" 21 | }, 22 | "devDependencies": { 23 | "mocha": "^2.0.1", 24 | "substance-test": "0.2.x" 25 | }, 26 | "scripts": { 27 | "test": "mocha -R spec tests/run.js" 28 | }, 29 | "version": "0.8.0", 30 | "engines": { 31 | "node": ">=0.8.x", 32 | "npm": ">=1.1.x" 33 | } 34 | } -------------------------------------------------------------------------------- /src/graph.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('underscore'); 4 | var util = require('substance-util'); 5 | var errors = util.errors; 6 | 7 | var Schema = require('./schema'); 8 | var Property = require('./property'); 9 | var Index = require('./graph_index'); 10 | 11 | var GraphError = errors.define("GraphError"); 12 | 13 | // Data types registry 14 | // ------------------- 15 | // Available data types for graph properties. 16 | 17 | var VALUE_TYPES = [ 18 | 'object', 19 | 'array', 20 | 'string', 21 | 'number', 22 | 'boolean', 23 | 'date' 24 | ]; 25 | 26 | 27 | // Check if composite type is in types registry. 28 | // The actual type of a composite type is the first entry 29 | // I.e., ["array", "string"] is an array in first place. 30 | var isValueType = function (type) { 31 | if (_.isArray(type)) { 32 | type = type[0]; 33 | } 34 | return VALUE_TYPES.indexOf(type) >= 0; 35 | }; 36 | 37 | // Graph 38 | // ===== 39 | 40 | // A `Graph` can be used for representing arbitrary complex object 41 | // graphs. Relations between objects are expressed through links that 42 | // point to referred objects. Graphs can be traversed in various ways. 43 | // See the testsuite for usage. 44 | // 45 | // Need to be documented: 46 | // @options (mode,seed,chronicle,store,load,graph) 47 | var Graph = function(schema, options) { 48 | options = options || {}; 49 | 50 | // Initialization 51 | this.schema = new Schema(schema); 52 | 53 | // Check if provided seed conforms to the given schema 54 | // Only when schema has an id and seed is provided 55 | 56 | if (this.schema.id && options.seed && options.seed.schema) { 57 | if (!_.isEqual(options.seed.schema, [this.schema.id, this.schema.version])) { 58 | throw new GraphError([ 59 | "Graph does not conform to schema. Expected: ", 60 | this.schema.id+"@"+this.schema.version, 61 | " Actual: ", 62 | options.seed.schema[0]+"@"+options.seed.schema[1] 63 | ].join('')); 64 | } 65 | } 66 | 67 | this.nodes = {}; 68 | this.indexes = {}; 69 | 70 | this.__seed__ = options.seed; 71 | 72 | this.init(); 73 | }; 74 | 75 | Graph.Prototype = function() { 76 | 77 | // Graph manipulation API 78 | // ====================== 79 | 80 | // Add a new node 81 | // -------------- 82 | // Adds a new node to the graph 83 | // Only properties that are specified in the schema are taken: 84 | // var node = { 85 | // id: "apple", 86 | // type: "fruit", 87 | // name: "My Apple", 88 | // color: "red", 89 | // val: { size: "big" } 90 | // }; 91 | // Create new node: 92 | // Data.Graph.create(node); 93 | // Note: graph create operation should reject creation of duplicate nodes. 94 | 95 | _.extend(this, util.Events); 96 | 97 | this.create = function(node) { 98 | this.nodes[node.id] = node; 99 | this._updateIndexes({ 100 | type: 'create', 101 | path: [node.id], 102 | val: node 103 | }); 104 | }; 105 | 106 | // Remove a node 107 | // ------------- 108 | // Removes a node with given id and key (optional): 109 | // Data.Graph.delete(this.graph.get('apple')); 110 | this.delete = function(id) { 111 | var oldVal = this.nodes[id]; 112 | delete this.nodes[id]; 113 | this._updateIndexes({ 114 | type: 'delete', 115 | path: [id], 116 | val: oldVal 117 | }); 118 | }; 119 | 120 | // Set the property 121 | // ---------------- 122 | // 123 | // Sets the property to a given value: 124 | // Data.Graph.set(["fruit_2", "val", "size"], "too small"); 125 | // Let's see what happened with node: 126 | // var blueberry = this.graph.get("fruit_2"); 127 | // console.log(blueberry.val.size); 128 | // = > 'too small' 129 | 130 | this.set = function(path, newValue) { 131 | var prop = this.resolve(path); 132 | if (!prop) { 133 | throw new GraphError("Could not resolve property with path "+JSON.stringify(path)); 134 | } 135 | var oldVal = prop.get(); 136 | prop.set(newValue); 137 | this._updateIndexes({ 138 | type: 'set', 139 | path: path, 140 | val: newValue, 141 | original: oldVal 142 | }); 143 | }; 144 | 145 | // Get the node [property] 146 | // ----------------------- 147 | // 148 | // Gets specified graph node using id: 149 | // var apple = this.graph.get("apple"); 150 | // console.log(apple); 151 | // => 152 | // { 153 | // id: "apple", 154 | // type: "fruit", 155 | // name: "My Apple", 156 | // color: "red", 157 | // val: { size: "big" } 158 | // } 159 | // or get node's property: 160 | // var apple = this.graph.get(["apple","color"]); 161 | // console.log(apple); 162 | // => 'red' 163 | 164 | this.get = function(path) { 165 | if (!_.isArray(path) && !_.isString(path)) { 166 | throw new GraphError("Invalid argument path. Must be String or Array"); 167 | } 168 | 169 | if (arguments.length > 1) path = _.toArray(arguments); 170 | if (_.isString(path)) return this.nodes[path]; 171 | 172 | var prop = this.resolve(path); 173 | return prop.get(); 174 | }; 175 | 176 | // Query graph data 177 | // ---------------- 178 | // 179 | // Perform smart querying on graph 180 | // graph.create({ 181 | // id: "apple-tree", 182 | // type: "tree", 183 | // name: "Apple tree" 184 | // }); 185 | // var apple = this.graph.get("apple"); 186 | // apple.set({["apple","tree"], "apple-tree"}); 187 | // let's perform query: 188 | // var result = graph.query(["apple", "tree"]); 189 | // console.log(result); 190 | // => [{id: "apple-tree", type: "tree", name: "Apple tree"}] 191 | 192 | this.query = function(path) { 193 | var prop = this.resolve(path); 194 | 195 | var type = prop.type; 196 | var baseType = prop.baseType; 197 | var val = prop.get(); 198 | 199 | // resolve referenced nodes in array types 200 | if (baseType === "array") { 201 | return this._queryArray.call(this, val, type); 202 | } else if (!isValueType(baseType)) { 203 | return this.get(val); 204 | } else { 205 | return val; 206 | } 207 | }; 208 | 209 | // Serialize current state 210 | // ----------------------- 211 | // 212 | // Convert current graph state to JSON object 213 | 214 | this.toJSON = function() { 215 | return { 216 | id: this.id, 217 | schema: [this.schema.id, this.schema.version], 218 | nodes: util.deepclone(this.nodes) 219 | }; 220 | }; 221 | 222 | // Check node existing 223 | // ------------------- 224 | // 225 | // Checks if a node with given id exists 226 | // this.graph.contains("apple"); 227 | // => true 228 | // this.graph.contains("orange"); 229 | // => false 230 | 231 | this.contains = function(id) { 232 | return (!!this.nodes[id]); 233 | }; 234 | 235 | // Resolve a property 236 | // ------------------ 237 | // Resolves a property with a given path 238 | 239 | this.resolve = function(path) { 240 | return new Property(this, path); 241 | }; 242 | 243 | // Reset to initial state 244 | // ---------------------- 245 | // Resets the graph to its initial state. 246 | // Note: This clears all nodes and calls `init()` which may seed the graph. 247 | 248 | this.reset = function() { 249 | this.init(); 250 | this.trigger("graph:reset"); 251 | }; 252 | 253 | // Graph initialization. 254 | this.init = function() { 255 | this.__is_initializing__ = true; 256 | 257 | if (this.__seed__) { 258 | this.nodes = util.clone(this.__seed__.nodes); 259 | } else { 260 | this.nodes = {}; 261 | } 262 | 263 | _.each(this.indexes, function(index) { 264 | index.reset(); 265 | }); 266 | 267 | delete this.__is_initializing__; 268 | }; 269 | 270 | this.addIndex = function(name, options) { 271 | if (this.indexes[name]) { 272 | throw new GraphError("Index with name " + name + "already exists."); 273 | } 274 | var index = new Index(this, options); 275 | this.indexes[name] = index; 276 | 277 | return index; 278 | }; 279 | 280 | this.removeIndex = function(name) { 281 | delete this.indexes[name]; 282 | }; 283 | 284 | this._updateIndexes = function(op) { 285 | _.each(this.indexes, function(index) { 286 | if (!op) { 287 | index.rebuild(); 288 | } else { 289 | index.onGraphChange(op); 290 | } 291 | }, this); 292 | }; 293 | 294 | this._queryArray = function(arr, type) { 295 | if (!_.isArray(type)) { 296 | throw new GraphError("Illegal argument: array types must be specified as ['array'(, 'array')*, ]"); 297 | } 298 | var result, idx; 299 | if (type[1] === "array") { 300 | result = []; 301 | for (idx = 0; idx < arr.length; idx++) { 302 | result.push(this._queryArray(arr[idx], type.slice(1))); 303 | } 304 | } else if (!isValueType(type[1])) { 305 | result = []; 306 | for (idx = 0; idx < arr.length; idx++) { 307 | result.push(this.get(arr[idx])); 308 | } 309 | } else { 310 | result = arr; 311 | } 312 | return result; 313 | }; 314 | 315 | }; 316 | 317 | // Index Modes 318 | // ---------- 319 | 320 | Graph.STRICT_INDEXING = 1 << 1; 321 | Graph.DEFAULT_MODE = Graph.STRICT_INDEXING; 322 | 323 | 324 | Graph.prototype = new Graph.Prototype(); 325 | 326 | Graph.Schema = Schema; 327 | Graph.Property = Property; 328 | Graph.Index = Index; 329 | 330 | // Exports 331 | // ======== 332 | 333 | module.exports = Graph; 334 | -------------------------------------------------------------------------------- /src/graph_index.js: -------------------------------------------------------------------------------- 1 | var _ = require("underscore"); 2 | var util = require("substance-util"); 3 | 4 | // Creates an index for the document applying a given node filter function 5 | // and grouping using a given key function 6 | // -------- 7 | // 8 | // - document: a document instance 9 | // - filter: a function that takes a node and returns true if the node should be indexed 10 | // - key: a function that provides a path for scoped indexing (default: returns empty path) 11 | // 12 | 13 | var Index = function(graph, options) { 14 | options = options || {}; 15 | 16 | this.graph = graph; 17 | 18 | this.nodes = {}; 19 | this.scopes = {}; 20 | 21 | if (options.filter) { 22 | this.filter = options.filter; 23 | } else if (options.types) { 24 | this.filter = Index.typeFilter(graph.schema, options.types); 25 | } 26 | 27 | if (options.property) { 28 | this.property = options.property; 29 | } 30 | 31 | this.createIndex(); 32 | }; 33 | 34 | Index.Prototype = function() { 35 | 36 | // Resolves a sub-hierarchy of the index via a given path 37 | // -------- 38 | // 39 | 40 | var _resolve = function(path) { 41 | var index = this; 42 | if (path !== null) { 43 | for (var i = 0; i < path.length; i++) { 44 | var id = path[i]; 45 | index.scopes[id] = index.scopes[id] || { nodes: {}, scopes: {} }; 46 | index = index.scopes[id]; 47 | } 48 | } 49 | return index; 50 | }; 51 | 52 | var _getKey = function(node) { 53 | if (!this.property) return null; 54 | var key = node[this.property] ? node[this.property] : null; 55 | if (_.isString(key)) key = [key]; 56 | return key; 57 | }; 58 | 59 | // Accumulates all indexed children of the given (sub-)index 60 | var _collect = function(index) { 61 | var result = _.extend({}, index.nodes); 62 | _.each(index.scopes, function(child, name) { 63 | if (name !== "nodes") { 64 | _.extend(result, _collect(child)); 65 | } 66 | }); 67 | return result; 68 | }; 69 | 70 | // Keeps the index up-to-date when the graph changes. 71 | // -------- 72 | // 73 | 74 | this.onGraphChange = function(op) { 75 | this.applyOp(op); 76 | }; 77 | 78 | this._add = function(node) { 79 | if (!this.filter || this.filter(node)) { 80 | var key = _getKey.call(this, node); 81 | var index = _resolve.call(this, key); 82 | index.nodes[node.id] = node.id; 83 | } 84 | }; 85 | 86 | this._remove = function(node) { 87 | if (!this.filter || this.filter(node)) { 88 | var key = _getKey.call(this, node); 89 | var index = _resolve.call(this, key); 90 | delete index.nodes[node.id]; 91 | } 92 | }; 93 | 94 | this._update = function(node, property, newValue, oldValue) { 95 | if ((this.property === property) && (!this.filter || this.filter(node))) { 96 | var key = oldValue; 97 | if (_.isString(key)) key = [key]; 98 | var index = _resolve.call(this, key); 99 | delete index.nodes[node.id]; 100 | key = newValue; 101 | index.nodes[node.id] = node.id; 102 | } 103 | }; 104 | 105 | 106 | this.applyOp = function(op) { 107 | if (op.type === "create") { 108 | this._add(op.val); 109 | } 110 | else if (op.type === "delete") { 111 | this._remove(op.val); 112 | } 113 | // type = 'update' or 'set' 114 | else { 115 | var prop = this.graph.resolve(this, op.path); 116 | var value = prop.get(); 117 | var oldValue; 118 | if (value === undefined) { 119 | return; 120 | } 121 | if (op.type === "set") { 122 | oldValue = op.original; 123 | } else { 124 | console.error("Operational updates are not supported in this implementation"); 125 | } 126 | this._update(prop.node, prop.key, value, oldValue); 127 | } 128 | }; 129 | 130 | // Initializes the index 131 | // -------- 132 | // 133 | 134 | this.createIndex = function() { 135 | this.reset(); 136 | 137 | var nodes = this.graph.nodes; 138 | _.each(nodes, function(node) { 139 | if (!this.filter || this.filter(node)) { 140 | var key = _getKey.call(this, node); 141 | var index = _resolve.call(this, key); 142 | index.nodes[node.id] = node.id; 143 | } 144 | }, this); 145 | }; 146 | 147 | // Collects all indexed nodes using a given path for scoping 148 | // -------- 149 | // 150 | 151 | this.get = function(path) { 152 | if (arguments.length === 0) { 153 | path = null; 154 | } else if (_.isString(path)) { 155 | path = [path]; 156 | } 157 | 158 | var index = _resolve.call(this, path); 159 | 160 | // EXPERIMENTAL: do we need the ability to retrieve indexed elements non-recursively 161 | // for now... 162 | // if so... we would need an paramater to prevent recursion 163 | // E.g.: 164 | // if (shallow) { 165 | // result = index.nodes; 166 | // } 167 | var collected = _collect(index); 168 | var result = new Index.Result(); 169 | 170 | _.each(collected, function(id) { 171 | result[id] = this.graph.get(id); 172 | }, this); 173 | 174 | return result; 175 | }; 176 | 177 | this.reset = function() { 178 | this.nodes = {}; 179 | this.scopes = {}; 180 | }; 181 | 182 | this.dispose = function() { 183 | this.stopListening(); 184 | }; 185 | 186 | this.rebuild = function() { 187 | this.reset(); 188 | this.createIndex(); 189 | }; 190 | }; 191 | 192 | Index.prototype = _.extend(new Index.Prototype(), util.Events.Listener); 193 | 194 | Index.typeFilter = function(schema, types) { 195 | return function(node) { 196 | var typeChain = schema.typeChain(node.type); 197 | for (var i = 0; i < types.length; i++) { 198 | if (typeChain.indexOf(types[i]) >= 0) { 199 | return true; 200 | } 201 | } 202 | return false; 203 | }; 204 | }; 205 | 206 | Index.Result = function() {}; 207 | Index.Result.prototype.asList = function() { 208 | var list = []; 209 | for (var key in this) { 210 | list.push(this[key]); 211 | } 212 | }; 213 | Index.Result.prototype.getLength = function() { 214 | return Object.keys(this).length; 215 | }; 216 | 217 | module.exports = Index; 218 | -------------------------------------------------------------------------------- /src/property.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("underscore"); 4 | 5 | var Property = function(graph, path) { 6 | if (!path) { 7 | throw new Error("Illegal argument: path is null/undefined."); 8 | } 9 | 10 | this.graph = graph; 11 | this.schema = graph.schema; 12 | 13 | _.extend(this, this.resolve(path)); 14 | }; 15 | 16 | Property.Prototype = function() { 17 | 18 | this.resolve = function(path) { 19 | var node = this.graph; 20 | var parent = node; 21 | var type = "graph"; 22 | 23 | var key; 24 | var value; 25 | 26 | var idx = 0; 27 | for (; idx < path.length; idx++) { 28 | 29 | // TODO: check if the property references a node type 30 | if (type === "graph" || this.schema.types[type] !== undefined) { 31 | // remember the last node type 32 | parent = this.graph.get(path[idx]); 33 | 34 | if (parent === undefined) { 35 | //throw new Error("Key error: could not find element for path " + JSON.stringify(path)); 36 | return undefined; 37 | } 38 | 39 | node = parent; 40 | type = this.schema.properties(parent.type); 41 | value = node; 42 | key = undefined; 43 | } else { 44 | if (parent === undefined) { 45 | //throw new Error("Key error: could not find element for path " + JSON.stringify(path)); 46 | return undefined; 47 | } 48 | key = path[idx]; 49 | var propName = path[idx]; 50 | type = type[propName]; 51 | value = parent[key]; 52 | 53 | if (idx < path.length-1) { 54 | parent = parent[propName]; 55 | } 56 | } 57 | } 58 | 59 | return { 60 | node: node, 61 | parent: parent, 62 | type: type, 63 | key: key, 64 | value: value 65 | }; 66 | 67 | }; 68 | 69 | this.get = function() { 70 | if (this.key !== undefined) { 71 | return this.parent[this.key]; 72 | } else { 73 | return this.node; 74 | } 75 | }; 76 | 77 | this.set = function(value) { 78 | if (this.key !== undefined) { 79 | this.parent[this.key] = this.schema.parseValue(this.baseType, value); 80 | } else { 81 | throw new Error("'set' is only supported for node properties."); 82 | } 83 | }; 84 | 85 | }; 86 | Property.prototype = new Property.Prototype(); 87 | Object.defineProperties(Property.prototype, { 88 | baseType: { 89 | get: function() { 90 | if (_.isArray(this.type)) return this.type[0]; 91 | else return this.type; 92 | } 93 | }, 94 | path: { 95 | get: function() { 96 | return [this.node.id, this.key]; 97 | } 98 | } 99 | }); 100 | 101 | module.exports = Property; 102 | -------------------------------------------------------------------------------- /src/schema.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("underscore"); 4 | var util = require("substance-util"); 5 | 6 | 7 | // Data.Schema 8 | // ======== 9 | // 10 | // Provides a schema inspection API 11 | 12 | var Schema = function(schema) { 13 | _.extend(this, schema); 14 | }; 15 | 16 | Schema.Prototype = function() { 17 | 18 | // Return Default value for a given type 19 | // -------- 20 | // 21 | 22 | this.defaultValue = function(valueType) { 23 | if (valueType === "object") return {}; 24 | if (valueType === "array") return []; 25 | if (valueType === "string") return ""; 26 | if (valueType === "number") return 0; 27 | if (valueType === "boolean") return false; 28 | if (valueType === "date") return new Date(); 29 | 30 | return null; 31 | // throw new Error("Unknown value type: " + valueType); 32 | }; 33 | 34 | // Return type object for a given type id 35 | // -------- 36 | // 37 | 38 | this.parseValue = function(valueType, value) { 39 | if (value === null) { 40 | return value; 41 | } 42 | 43 | if (_.isString(value)) { 44 | if (valueType === "object") return JSON.parse(value); 45 | if (valueType === "array") return JSON.parse(value); 46 | if (valueType === "string") return value; 47 | if (valueType === "number") return parseInt(value, 10); 48 | if (valueType === "boolean") { 49 | if (value === "true") return true; 50 | else if (value === "false") return false; 51 | else throw new Error("Can not parse boolean value from: " + value); 52 | } 53 | if (valueType === "date") return new Date(value); 54 | 55 | // all other types must be string compatible ?? 56 | return value; 57 | 58 | } else { 59 | if (valueType === 'array') { 60 | if (!_.isArray(value)) { 61 | throw new Error("Illegal value type: expected array."); 62 | } 63 | value = util.deepclone(value); 64 | } 65 | else if (valueType === 'string') { 66 | if (!_.isString(value)) { 67 | throw new Error("Illegal value type: expected string."); 68 | } 69 | } 70 | else if (valueType === 'object') { 71 | if (!_.isObject(value)) { 72 | throw new Error("Illegal value type: expected object."); 73 | } 74 | value = util.deepclone(value); 75 | } 76 | else if (valueType === 'number') { 77 | if (!_.isNumber(value)) { 78 | throw new Error("Illegal value type: expected number."); 79 | } 80 | } 81 | else if (valueType === 'boolean') { 82 | if (!_.isBoolean(value)) { 83 | throw new Error("Illegal value type: expected boolean."); 84 | } 85 | } 86 | else if (valueType === 'date') { 87 | value = new Date(value); 88 | } 89 | else { 90 | throw new Error("Unsupported value type: " + valueType); 91 | } 92 | return value; 93 | } 94 | }; 95 | 96 | // Return type object for a given type id 97 | // -------- 98 | // 99 | 100 | this.type = function(typeId) { 101 | return this.types[typeId]; 102 | }; 103 | 104 | // For a given type id return the type hierarchy 105 | // -------- 106 | // 107 | // => ["base_type", "specific_type"] 108 | 109 | this.typeChain = function(typeId) { 110 | var type = this.types[typeId]; 111 | if (!type) { 112 | throw new Error('Type ' + typeId + ' not found in schema'); 113 | } 114 | 115 | var chain = (type.parent) ? this.typeChain(type.parent) : []; 116 | chain.push(typeId); 117 | return chain; 118 | }; 119 | 120 | this.isInstanceOf = function(type, parentType) { 121 | var typeChain = this.typeChain(type); 122 | if (typeChain && typeChain.indexOf(parentType) >= 0) { 123 | return true; 124 | } else { 125 | return false; 126 | } 127 | }; 128 | 129 | // Provides the top-most parent type of a given type. 130 | // -------- 131 | // 132 | 133 | this.baseType = function(typeId) { 134 | return this.typeChain(typeId)[0]; 135 | }; 136 | 137 | // Return all properties for a given type 138 | // -------- 139 | // 140 | 141 | this.properties = function(type) { 142 | type = _.isObject(type) ? type : this.type(type); 143 | var result = (type.parent) ? this.properties(type.parent) : {}; 144 | _.extend(result, type.properties); 145 | return result; 146 | }; 147 | 148 | // Returns the full type for a given property 149 | // -------- 150 | // 151 | // => ["array", "string"] 152 | 153 | this.propertyType = function(type, property) { 154 | var properties = this.properties(type); 155 | var propertyType = properties[property]; 156 | if (!propertyType) throw new Error("Property not found for" + type +'.'+property); 157 | return _.isArray(propertyType) ? propertyType : [propertyType]; 158 | }; 159 | 160 | // Returns the base type for a given property 161 | // -------- 162 | // 163 | // ["string"] => "string" 164 | // ["array", "string"] => "array" 165 | 166 | this.propertyBaseType = function(type, property) { 167 | return this.propertyType(type, property)[0]; 168 | }; 169 | }; 170 | 171 | Schema.prototype = new Schema.Prototype(); 172 | 173 | module.exports = Schema; 174 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | 2 | require("./schema_test"); 3 | -------------------------------------------------------------------------------- /tests/run.js: -------------------------------------------------------------------------------- 1 | var Test = require("substance-test"); 2 | 3 | require("./index"); 4 | 5 | new Test.MochaRunner().run(); 6 | -------------------------------------------------------------------------------- /tests/schema_test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Import 4 | // ======== 5 | 6 | var _ = require('underscore'); 7 | var Test = require('substance-test'); 8 | var assert = Test.assert; 9 | var registerTest = Test.registerTest; 10 | var Data = require('../index'); 11 | 12 | 13 | // Test 14 | // ======== 15 | 16 | var SCHEMA = { 17 | id: "schema-1", 18 | version: "1.0.0", 19 | types: { 20 | elem: { 21 | properties: { 22 | obj: "object", 23 | arr: "array", 24 | str: "string", 25 | num: "number", 26 | flag: "boolean", 27 | time: "date", 28 | } 29 | }, 30 | node: { 31 | properties: { 32 | name: "string", 33 | } 34 | }, 35 | numbers: { 36 | parent: "node", 37 | properties: { 38 | val: ["number"], 39 | arr: ["array", "number"] 40 | } 41 | }, 42 | } 43 | }; 44 | 45 | var SchemaTest = function() { 46 | 47 | this.setup = function() { 48 | this.graph = new Data.Graph(SCHEMA); 49 | this.schema = this.graph.schema; 50 | }; 51 | 52 | this.actions = [ 53 | 54 | "Schema.defaultValue()", function() { 55 | 56 | assert.isDeepEqual({}, this.schema.defaultValue("object")); 57 | assert.isArrayEqual([], this.schema.defaultValue("array")); 58 | assert.isEqual("", this.schema.defaultValue("string")); 59 | assert.isEqual(0, this.schema.defaultValue("number")); 60 | assert.isEqual(false, this.schema.defaultValue("boolean")); 61 | // can only check if a default date is given 62 | assert.isDefined(this.schema.defaultValue("date")); 63 | }, 64 | 65 | "Schema.parseValue()", function() { 66 | assert.isDeepEqual({a: "bla"}, this.schema.parseValue("object", '{"a": "bla"}')); 67 | assert.isArrayEqual([1,2,3], this.schema.parseValue("array", '[1,2,3]')); 68 | assert.isEqual("bla", this.schema.parseValue("string", 'bla')); 69 | assert.isEqual(42, this.schema.parseValue("number", '42')); 70 | assert.isEqual(true, this.schema.parseValue("boolean", 'true')); 71 | var expected = new Date(Date.now()); 72 | var parsedDate = this.schema.parseValue("date", expected.toISOString()); 73 | assert.isEqual(expected.getTime(), parsedDate.getTime()); 74 | }, 75 | 76 | "Schema.properties()", function() { 77 | var props = this.schema.properties("elem"); 78 | 79 | assert.isDeepEqual(SCHEMA.types.elem.properties, props); 80 | // props should be a copy 81 | props["ooooh"] = "aaaaahh"; 82 | assert.isFalse(_.isEqual(SCHEMA.types.elem.properties, props)); 83 | }, 84 | 85 | "Inheritance - type chain", function() { 86 | var chain = this.schema.typeChain("numbers"); 87 | assert.isArrayEqual(["node", "numbers"], chain); 88 | }, 89 | 90 | "Inheritance - properties", function() { 91 | var expected = _.extend({}, SCHEMA.types.node.properties, SCHEMA.types.numbers.properties); 92 | var actual = this.schema.properties("numbers"); 93 | assert.isDeepEqual(expected, actual); 94 | }, 95 | 96 | "Composite types", function() { 97 | var expected = ["array", "number"]; 98 | var actual = this.schema.propertyType("numbers", "arr"); 99 | assert.isArrayEqual(expected, actual); 100 | } 101 | ]; 102 | }; 103 | 104 | registerTest(['Substance.Data', 'Schema'], new SchemaTest()); 105 | --------------------------------------------------------------------------------