summaryrefslogtreecommitdiff
path: root/app/node_modules/okschema/index.js
blob: 82aa13f9be89708c68c94b295abaab359b4d41e0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
var cloneDeep = require('lodash.clonedeep');
var mschema = require('mschema');
var v = require('validator');

/**
 * Custom types.
 * To add a custom type, add a key with the type name and a value
 * in the form {parent: {...}, assertValid: function(d) {...}}
 * where parent is the base mschema spec and validate is a function
 * accepting one data point and throwing an array of errors if invalid.
 *
 * Error array format is derived from mschema and is in the form
 * [{property: x, constraint: x, actual: x, expected: x, message: x} ... ]
 */
var types = {
  /**
   * Larger text inputs. Currently just proxies to string type.
   */
  'text': {
    parent: {type: 'string'},
    // Let parent handle validation
    assertValid: function(spec, value) {}
  },
  'video': {
    parent: {type: 'string'},
    // Let parent handle validation
    assertValid: function(spec, value) {}
  },
  'enum': {
    parent: {type: 'string'},
    assertValid: function(spec, value) {
      value = value || '';
      if (spec.options.indexOf(value.trim()) === -1) {
        throw [{
          constraint: 'enum',
          actual: value,
          expected: JSON.stringify(spec.options),
          message: 'Invalid value'
        }];
      }
    }
  },
  'captioned-image-list': {
    parent: [{
      uri: { type: 'string' }, // TODO Implement URI type
      caption: { type: 'string' }
    }],
    assertValid: function(spec, value) {
      var message;
      var actual;
      if (!value || !value.length) {
        throw [{
          message: 'Not an array',
          expected: JSON.stringify(this.parent),
          actual: value
        }];
      }
    }
  },
  // Special type for resource meta information
  'meta': {
    parent: 'string',
    assertValid: function(spec, value) {}
  },
  'tag-list': {
    parent: [{
      uri: { type: 'string' },
      text: { type: 'string' }
    }],
    assertValid: function(spec, value) {
      var message;
      var actual;
      if (!value || !value.length) {
        throw [{
          message: 'Not an array',
          expected: JSON.stringify(this.parent),
          actual: value
        }];
      }
    }
  },
  'date': {
    parent: 'string',
    assertValid: function(spec, value) {}
  },
  'flag': {
    parent: 'string',
    assertValid: function(spec, value) {}
  },
  'foreign-key': {
    parent: 'enum',
    assertValid: function(spec, value) {}
  }
}

/**
 * OKSchema!
 * Meant as a thin wrapper around some existing schema validation
 * module, mostly to allow for the extension of types.
 *
 * NOTE: Currently just assumes spec is valid. If you give a bad spec
 *       strange things may or may not happen
 */
function OKSchema(spec) {
  if (!(this instanceof OKSchema)) return new OKSchema(spec);
  if (!spec)
    throw new Error('No spec provided to OKSchema');
  spec = cloneDeep(spec);
  var specKeys = Object.keys(spec);
  // Cache the mschema version of our spec
  this._mschemaSpec = specKeys.reduce(function(cache, prop) {
    // If custom type, return its parent spec
    var type = spec[prop].type;
    if (types[type]) {
      cache[prop] = types[type].parent;
    // Otherwise, it's already in mschema format
    } else {
      cache[prop] = spec[prop];
    }
    return cache;
  }, {});

  // Find ID field
  var idField;
  specKeys.every(function(prop) {
    if (prop === 'id' || spec[prop].id) {
      idField = prop;
      return false;
    } else {
      return true;
    }
  });

  // Register autoincrement fields
  // NOTE Does not work for nested fields
  var autoIncrementFields = specKeys.reduce(function(arr, prop) {
    var specProp = spec[prop];
    if (specProp.autoincrement) {
      arr.push(prop);
    }
    return arr;
  }, []);

  Object.defineProperty(this, 'spec', {
    get: function() {
      return cloneDeep(spec);
    },
    enumerable: true
  });

  Object.defineProperty(this, 'idField', {
    value: idField,
    writable: true,
    enumerable: true
  });

  Object.defineProperty(this, 'autoIncrementFields',{
    get: function() {
      return cloneDeep(autoIncrementFields);
    },
    enumerable: true
  });
}

OKSchema.prototype.assertValid = function(data) {
  data = data || {};
  var spec = this.spec;
  // Run through custom validators, they'll throw if invalid
  Object.keys(data).forEach(function(prop) {
    var type = spec[prop].type;
    if (types[type]) {
      types[type].assertValid(spec[prop], data[prop]);
    // Also check if it's a number type and try to cast it
    // otherwise pass and let mschema handle
    } else if (type === 'number') {
      try {
        data[prop] = parseFloat(data[prop]);
      } catch (err) {}
    }
  });
  var result = mschema.validate(data, this.toMschema());
  if (!result.valid) {
    throw result.errors;
  }
};

/**
 * Return our custom spec as an mschema spec
 */
OKSchema.prototype.toMschema = function() {
  return this._mschemaSpec;
};

module.exports = OKSchema;