UNPKG

14.4 kBJavaScriptView Raw
1const Error = require("./Error");
2const utils = require("./utils");
3const Internal = require("./Internal");
4const Document = require("./Document");
5const internalCache = Internal.Schema.internalCache;
6
7class Schema {
8 constructor(object, settings = {}) {
9 if (!object || typeof object !== "object" || Array.isArray(object)) {
10 throw new Error.InvalidParameterType("Schema initalization parameter must be an object.");
11 }
12 if (Object.keys(object).length === 0) {
13 throw new Error.InvalidParameter("Schema initalization parameter must not be an empty object.");
14 }
15
16 if (settings.timestamps === true) {
17 settings.timestamps = {
18 "createdAt": "createdAt",
19 "updatedAt": "updatedAt"
20 };
21 }
22 if (settings.timestamps) {
23 if (object[settings.timestamps.createdAt] || object[settings.timestamps.updatedAt]) {
24 throw new Error.InvalidParameter("Timestamp attributes must not be defined in schema.");
25 }
26
27 object[settings.timestamps.createdAt] = Date;
28 object[settings.timestamps.updatedAt] = Date;
29 }
30
31 // Anytime `this.schemaObject` is modified, `this[internalCache].attributes` must be set to undefined or null
32 this.schemaObject = object;
33 this.settings = settings;
34 Object.defineProperty(this, internalCache, {
35 "configurable": false,
36 "value": {
37 "getAttributeTypeDetails": {}
38 }
39 });
40
41 const checkAttributeNameDots = (object/*, existingKey = ""*/) => {
42 return Object.keys(object).forEach((key) => {
43 if (key.includes(".")) {
44 throw new Error.InvalidParameter("Attributes must not contain dots.");
45 }
46
47 if (typeof object[key] === "object" && object[key].schema) {
48 checkAttributeNameDots(object[key].schema, key);
49 }
50 });
51 };
52 checkAttributeNameDots(this.schemaObject);
53
54 const checkMultipleArraySchemaElements = (key) => {
55 let attributeType;
56 try {
57 attributeType = this.getAttributeType(key);
58 } catch (e) {} // eslint-disable-line no-empty
59
60 if (attributeType === "L" && (this.getAttributeValue(key).schema || []).length > 1) {
61 throw new Error.InvalidParameter("You must only pass one element into schema array.");
62 }
63 };
64 this.attributes().forEach((key) => checkMultipleArraySchemaElements(key));
65 }
66}
67
68Schema.prototype.getHashKey = function() {
69 return Object.keys(this.schemaObject).find((key) => this.schemaObject[key].hashKey) || Object.keys(this.schemaObject)[0];
70};
71Schema.prototype.getRangeKey = function() {
72 return Object.keys(this.schemaObject).find((key) => this.schemaObject[key].rangeKey);
73};
74
75Schema.prototype.getAttributeSettingValue = function(setting, key, settings = {}) {
76 const defaultPropertyValue = (this.getAttributeValue(key) || {})[setting];
77 return typeof defaultPropertyValue === "function" && !settings.returnFunction ? defaultPropertyValue() : defaultPropertyValue;
78};
79
80// This function will take in an attribute and value, and throw an error if the property is required and the value is undefined or null.
81Schema.prototype.requiredCheck = async function(key, value) {
82 const isRequired = await this.getAttributeSettingValue("required", key);
83 if ((typeof value === "undefined" || value === null) && isRequired) {
84 throw new Error.ValidationError(`${key} is a required property but has no value when trying to save document`);
85 }
86};
87// This function will take in an attribute and value, and returns the default value if it should be applied.
88Schema.prototype.defaultCheck = async function(key, value, settings) {
89 const isValueUndefined = typeof value === "undefined" || value === null;
90 if ((settings.defaults && isValueUndefined) || (settings.forceDefault && await this.getAttributeSettingValue("forceDefault", key))) {
91 const defaultValue = await this.getAttributeSettingValue("default", key);
92 const isDefaultValueUndefined = typeof defaultValue === "undefined" || defaultValue === null;
93 if (!isDefaultValueUndefined) {
94 return defaultValue;
95 }
96 }
97};
98
99Schema.prototype.getIndexAttributes = async function() {
100 return (await Promise.all(this.attributes().map(async (attribute) => ({"index": await this.getAttributeSettingValue("index", attribute), attribute})))).filter((obj) => obj.index);
101};
102Schema.prototype.getIndexRangeKeyAttributes = async function() {
103 const indexes = await this.getIndexAttributes();
104 return indexes.map((index) => index.index.rangeKey).filter((a) => Boolean(a)).map((a) => ({"attribute": a}));
105};
106Schema.prototype.getIndexes = async function(model) {
107 return (await this.getIndexAttributes()).reduce((accumulator, currentValue) => {
108 let indexValue = currentValue.index;
109 const attributeValue = currentValue.attribute;
110
111 const dynamoIndexObject = {
112 "IndexName": indexValue.name || `${attributeValue}${indexValue.global ? "GlobalIndex" : "LocalIndex"}`,
113 "KeySchema": [{"AttributeName": attributeValue, "KeyType": "HASH"}],
114 "Projection": {"ProjectionType": "KEYS_ONLY"}
115 };
116 if (indexValue.project || typeof indexValue.project === "undefined" || indexValue.project === null) {
117 dynamoIndexObject.Projection = Array.isArray(indexValue.project) ? ({"ProjectionType": "INCLUDE", "NonKeyAttributes": indexValue.project}) : ({"ProjectionType": "ALL"});
118 }
119 if (indexValue.rangeKey) {
120 dynamoIndexObject.KeySchema.push({"AttributeName": indexValue.rangeKey, "KeyType": "RANGE"});
121 }
122 if (indexValue.global) {
123 const throughputObject = utils.dynamoose.get_provisioned_throughput(indexValue.throughput ? indexValue : model.options);
124 if (throughputObject.ProvisionedThroughput) {
125 dynamoIndexObject.ProvisionedThroughput = throughputObject.ProvisionedThroughput;
126 }
127 }
128 if (!accumulator[(indexValue.global ? "GlobalSecondaryIndexes" : "LocalSecondaryIndexes")]) {
129 accumulator[(indexValue.global ? "GlobalSecondaryIndexes" : "LocalSecondaryIndexes")] = [];
130 }
131 accumulator[(indexValue.global ? "GlobalSecondaryIndexes" : "LocalSecondaryIndexes")].push(dynamoIndexObject);
132
133 return accumulator;
134 }, {});
135};
136
137Schema.prototype.getSettingValue = function(setting) {
138 return this.settings[setting];
139};
140
141function attributes() {
142 const main = (object, existingKey = "") => {
143 return Object.keys(object).reduce((accumulator, key) => {
144 const keyWithExisting = `${existingKey ? `${existingKey}.` : ""}${key}`;
145 accumulator.push(keyWithExisting);
146
147 let attributeType;
148 try {
149 attributeType = this.getAttributeType(keyWithExisting);
150 } catch (e) {} // eslint-disable-line no-empty
151
152 if ((attributeType === "M" || attributeType === "L") && object[key].schema) {
153 accumulator.push(...main(object[key].schema, keyWithExisting));
154 }
155
156 return accumulator;
157 }, []);
158 };
159
160 return main(this.schemaObject);
161}
162Schema.prototype.attributes = function() {
163 if (!this[internalCache].attributes) {
164 this[internalCache].attributes = attributes.call(this);
165 }
166
167 return this[internalCache].attributes;
168};
169
170Schema.prototype.getAttributeValue = function(key, settings = {}) {
171 return (settings.standardKey ? key : key.replace(/\.\d+/gu, ".0")).split(".").reduce((result, part) => utils.object.get(result.schema, part), {"schema": this.schemaObject});
172};
173
174class DynamoDBType {
175 constructor(obj) {
176 Object.keys(obj).forEach((key) => {
177 this[key] = obj[key];
178 });
179 }
180
181 result(typeSettings) {
182 const isSubType = this.dynamodbType instanceof DynamoDBType; // Represents underlying DynamoDB type for custom types
183 const type = isSubType ? this.dynamodbType : this;
184 const result = {
185 "name": this.name,
186 "dynamodbType": isSubType ? this.dynamodbType.dynamodbType : this.dynamodbType,
187 "nestedType": this.nestedType
188 };
189 result.isOfType = this.jsType.func ? this.jsType.func : ((val) => {
190 return [{"value": this.jsType, "type": "main"}, {"value": (isSubType ? type.jsType : null), "type": "underlying"}].filter((a) => Boolean(a.value)).find((jsType) => typeof jsType.value === "string" ? typeof val === jsType.value : val instanceof jsType.value);
191 });
192 if (type.set) {
193 const typeName = type.customDynamoName || type.name;
194 result.set = {
195 "name": `${this.name} Set`,
196 "isSet": true,
197 "dynamodbType": `${type.dynamodbType}S`,
198 "isOfType": (val, type, settings = {}) => {
199 if (type === "toDynamo") {
200 return (!settings.saveUnknown && Array.isArray(val) && val.every((subValue) => result.isOfType(subValue))) || (val instanceof Set && [...val].every((subValue) => result.isOfType(subValue)));
201 } else {
202 return val.wrapperName === "Set" && val.type === typeName && Array.isArray(val.values);
203 }
204 },
205 "toDynamo": (val) => ({"wrapperName": "Set", "type": typeName, "values": [...val]}),
206 "fromDynamo": (val) => new Set(val.values)
207 };
208 if (this.customType) {
209 const functions = this.customType.functions(typeSettings);
210 result.customType = {
211 ...this.customType,
212 functions
213 };
214 result.set.customType = {
215 "functions": {
216 "toDynamo": (val) => val.map(functions.toDynamo),
217 "fromDynamo": (val) => ({"values": val.values.map(functions.fromDynamo)}),
218 "isOfType": (val, type) => {
219 if (type === "toDynamo") {
220 return Array.isArray(val) && val.every(functions.isOfType);
221 } else {
222 return val.wrapperName === "Set" && val.type === typeName && Array.isArray(val.values);
223 }
224 }
225 }
226 };
227 }
228 }
229
230 return result;
231 }
232}
233
234const attributeTypesMain = (() => {
235 const numberType = new DynamoDBType({"name": "Number", "dynamodbType": "N", "set": true, "jsType": "number"});
236 return [
237 new DynamoDBType({"name": "Buffer", "dynamodbType": "B", "set": true, "jsType": Buffer, "customDynamoName": "Binary"}),
238 new DynamoDBType({"name": "Boolean", "dynamodbType": "BOOL", "jsType": "boolean"}),
239 new DynamoDBType({"name": "Array", "dynamodbType": "L", "jsType": {"func": Array.isArray}, "nestedType": true}),
240 new DynamoDBType({"name": "Object", "dynamodbType": "M", "jsType": {"func": (val) => Boolean(val) && val.constructor === Object && (val.wrapperName !== "Set" || Object.keys(val).length !== 3 || !val.type || !val.values)}, "nestedType": true}),
241 numberType,
242 new DynamoDBType({"name": "String", "dynamodbType": "S", "set": true, "jsType": "string"}),
243 new DynamoDBType({"name": "Date", "dynamodbType": numberType, "customType": {
244 "functions": (typeSettings) => ({
245 "toDynamo": (val) => {
246 if (typeSettings.storage === "seconds") {
247 return Math.round(val.getTime() / 1000);
248 } else {
249 return val.getTime();
250 }
251 },
252 "fromDynamo": (val) => {
253 if (typeSettings.storage === "seconds") {
254 return new Date(val * 1000);
255 } else {
256 return new Date(val);
257 }
258 },
259 "isOfType": (val, type) => {
260 return type === "toDynamo" ? val instanceof Date : typeof val === "number";
261 }
262 })
263 }, "jsType": Date})
264 ];
265})();
266const attributeTypes = utils.array_flatten(attributeTypesMain.filter((checkType) => !checkType.customType).map((checkType) => checkType.result()).map((a) => [a, a.set])).filter((a) => Boolean(a));
267function retrieveTypeInfo(type, isSet, key, typeSettings) {
268 const foundType = attributeTypesMain.find((checkType) => checkType.name.toLowerCase() === type.toLowerCase());
269 if (!foundType) {
270 throw new Error.InvalidType(`${key} contains an invalid type: ${type}`);
271 }
272 const parentType = foundType.result(typeSettings);
273 if (!parentType.set && isSet) {
274 throw new Error.InvalidType(`${key} with type: ${type} is not allowed to be a set`);
275 }
276 return isSet ? parentType.set : parentType;
277}
278Schema.prototype.getAttributeTypeDetails = function(key, settings = {}) {
279 const standardKey = (settings.standardKey ? key : key.replace(/\.\d+/gu, ".0"));
280 if (this[internalCache].getAttributeTypeDetails[standardKey]) {
281 return this[internalCache].getAttributeTypeDetails[standardKey];
282 }
283 const val = this.getAttributeValue(standardKey, {"standardKey": true});
284 if (!val) {
285 throw new Error.UnknownAttribute(`Invalid Attribute: ${key}`);
286 }
287 let typeVal = typeof val === "object" && !Array.isArray(val) ? val.type : val;
288 let typeSettings = {};
289 if (typeof typeVal === "object" && !Array.isArray(typeVal)) {
290 typeSettings = typeVal.settings || {};
291 typeVal = typeVal.value;
292 }
293
294 const getType = (typeVal) => {
295 let type;
296 if (typeof typeVal === "function") {
297 const regexFuncName = /^Function ([^(]+)\(/iu;
298 [, type] = typeVal.toString().match(regexFuncName);
299 } else {
300 type = typeVal;
301 }
302 return type;
303 };
304 let type = getType(typeVal);
305 const isSet = type.toLowerCase() === "set";
306 if (isSet) {
307 type = getType(this.getAttributeSettingValue("schema", key)[0]);
308 }
309
310 const returnObject = retrieveTypeInfo(type, isSet, key, typeSettings);
311 this[internalCache].getAttributeTypeDetails[standardKey] = returnObject;
312 return returnObject;
313};
314Schema.prototype.getAttributeType = function(key, value, settings = {}) {
315 try {
316 return this.getAttributeTypeDetails(key).dynamodbType;
317 } catch (e) {
318 if (settings.unknownAttributeAllowed && e.message === `Invalid Attribute: ${key}` && value) {
319 return Object.keys(Document.toDynamo(value, {"type": "value"}))[0];
320 } else {
321 throw e;
322 }
323 }
324};
325
326Schema.prototype.getCreateTableAttributeParams = async function(model) {
327 const hashKey = this.getHashKey();
328 const AttributeDefinitions = [
329 {
330 "AttributeName": hashKey,
331 "AttributeType": this.getAttributeType(hashKey)
332 }
333 ];
334 const AttributeDefinitionsNames = [hashKey];
335 const KeySchema = [
336 {
337 "AttributeName": hashKey,
338 "KeyType": "HASH"
339 }
340 ];
341
342 const rangeKey = this.getRangeKey();
343 if (rangeKey) {
344 AttributeDefinitions.push({
345 "AttributeName": rangeKey,
346 "AttributeType": this.getAttributeType(rangeKey)
347 });
348 AttributeDefinitionsNames.push(rangeKey);
349 KeySchema.push({
350 "AttributeName": rangeKey,
351 "KeyType": "RANGE"
352 });
353 }
354
355 utils.array_flatten(await Promise.all([this.getIndexAttributes(), this.getIndexRangeKeyAttributes()])).map((obj) => obj.attribute).forEach((index) => {
356 if (AttributeDefinitionsNames.includes(index)) {
357 return;
358 }
359
360 AttributeDefinitionsNames.push(index);
361 AttributeDefinitions.push({
362 "AttributeName": index,
363 "AttributeType": this.getAttributeType(index)
364 });
365 });
366
367 return {
368 AttributeDefinitions,
369 KeySchema,
370 ...await this.getIndexes(model)
371 };
372};
373
374
375module.exports = Schema;
376module.exports.attributeTypes = {
377 "findDynamoDBType": (type) => attributeTypes.find((checkType) => checkType.dynamodbType === type),
378 "findTypeForValue": (...args) => attributeTypes.find((checkType) => checkType.isOfType(...args))
379};