1 | const aws = require("./aws");
|
2 | const ddb = require("./aws/ddb/internal");
|
3 | const utils = require("./utils");
|
4 | const Error = require("./Error");
|
5 | const {internalProperties} = require("./Internal").General;
|
6 |
|
7 | const staticMethods = {
|
8 | "toDynamo": (object, settings = {"type": "object"}) => (settings.type === "value" ? aws.converter().input : aws.converter().marshall)(object),
|
9 | "fromDynamo": (object) => aws.converter().unmarshall(object),
|
10 |
|
11 | "isDynamoObject": function (object, recurrsive = false) {
|
12 |
|
13 | const isValid = (value) => {
|
14 | if (typeof value === "undefined" || value === null) {
|
15 | return false;
|
16 | }
|
17 | const keys = Object.keys(value);
|
18 | const key = keys[0];
|
19 | const nestedResult = (typeof value[key] === "object" && !(value[key] instanceof Buffer) ? (Array.isArray(value[key]) ? value[key].every((value) => this.isDynamoObject(value, true)) : this.isDynamoObject(value[key])) : true);
|
20 | const Schema = require("./Schema");
|
21 | const attributeType = Schema.attributeTypes.findDynamoDBType(key);
|
22 | return typeof value === "object" && keys.length === 1 && attributeType && (nestedResult || Object.keys(value[key]).length === 0 || attributeType.isSet);
|
23 | };
|
24 |
|
25 | const keys = Object.keys(object);
|
26 | const values = Object.values(object);
|
27 | if (keys.length === 0) {
|
28 | return null;
|
29 | } else {
|
30 | return recurrsive ? isValid(object) : values.every((value) => isValid(value));
|
31 | }
|
32 | }
|
33 | };
|
34 | function applyStaticMethods(item) {
|
35 | Object.entries(staticMethods).forEach((entry) => {
|
36 | const [key, value] = entry;
|
37 | item[key] = value;
|
38 | });
|
39 | }
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | function DocumentCarrier(model) {
|
45 |
|
46 | class Document {
|
47 | constructor(object = {}, settings = {}) {
|
48 | const documentObject = Document.isDynamoObject(object) ? aws.converter().unmarshall(object) : object;
|
49 | Object.keys(documentObject).forEach((key) => this[key] = documentObject[key]);
|
50 | Object.defineProperty(this, internalProperties, {
|
51 | "configurable": false,
|
52 | "value": {}
|
53 | });
|
54 | this[internalProperties].originalObject = {...object};
|
55 | this[internalProperties].originalSettings = {...settings};
|
56 |
|
57 | if (settings.fromDynamo) {
|
58 | this[internalProperties].storedInDynamo = true;
|
59 | }
|
60 | }
|
61 | }
|
62 |
|
63 | applyStaticMethods(Document);
|
64 |
|
65 | Document.prepareForObjectFromSchema = function(object, settings) {
|
66 | if (settings.updateTimestamps) {
|
67 | if (model.schema.settings.timestamps && settings.type === "toDynamo") {
|
68 | const date = new Date();
|
69 |
|
70 | if (model.schema.settings.timestamps.createdAt && (object[internalProperties] && !object[internalProperties].storedInDynamo)) {
|
71 | object[model.schema.settings.timestamps.createdAt] = date;
|
72 | }
|
73 | if (model.schema.settings.timestamps.updatedAt && (typeof settings.updateTimestamps === "boolean" || settings.updateTimestamps.updatedAt)) {
|
74 | object[model.schema.settings.timestamps.updatedAt] = date;
|
75 | }
|
76 | }
|
77 | }
|
78 | return object;
|
79 | };
|
80 |
|
81 |
|
82 | const attributesWithSchemaCache = {};
|
83 | Document.attributesWithSchema = function(document) {
|
84 | const attributes = model.schema.attributes();
|
85 | if (attributesWithSchemaCache[document] && attributesWithSchemaCache[document][attributes]) {
|
86 | return attributesWithSchemaCache[document][attributes];
|
87 | }
|
88 |
|
89 | const root = {};
|
90 | attributes.forEach((attribute) => {
|
91 | let node = root;
|
92 | attribute.split(".").forEach((part) => {
|
93 | node[part] = node[part] || {};
|
94 | node = node[part];
|
95 | });
|
96 | });
|
97 |
|
98 | function traverse (node, treeNode, outPath, callback) {
|
99 | callback(outPath);
|
100 | if (Object.keys(treeNode).length === 0) {
|
101 | return;
|
102 | }
|
103 |
|
104 | Object.keys(treeNode).forEach((attr) => {
|
105 | if (attr === "0") {
|
106 | if (!node || node.length == 0) {
|
107 | node = [{}];
|
108 | }
|
109 | node.forEach((a, index) => {
|
110 | outPath.push(index);
|
111 | traverse(node[index], treeNode[attr], outPath, callback);
|
112 | outPath.pop();
|
113 | });
|
114 | } else {
|
115 | if (!node) {
|
116 | node = {};
|
117 | }
|
118 | outPath.push(attr);
|
119 | traverse(node[attr], treeNode[attr], outPath, callback);
|
120 | outPath.pop();
|
121 | }
|
122 | });
|
123 | }
|
124 | const out = [];
|
125 | traverse(document, root, [], (val) => out.push(val.join(".")));
|
126 | const result = out.slice(1);
|
127 | attributesWithSchemaCache[document] = {[attributes]: result};
|
128 | return result;
|
129 | };
|
130 |
|
131 | Document.objectFromSchema = async function(object, settings = {"type": "toDynamo"}) {
|
132 | if (settings.checkExpiredItem && model.options.expires && (model.options.expires.items || {}).returnExpired === false && object[model.options.expires.attribute] && (object[model.options.expires.attribute] * 1000) < Date.now()) {
|
133 | return undefined;
|
134 | }
|
135 |
|
136 | const returnObject = {...object};
|
137 | const schemaAttributes = model.schema.attributes();
|
138 | const dynamooseUndefined = require("./index").UNDEFINED;
|
139 |
|
140 |
|
141 | const validParents = [];
|
142 | const keysToDelete = [];
|
143 | const getValueTypeCheckResult = (value, key, options = {}) => {
|
144 | const typeDetails = model.schema.getAttributeTypeDetails(key, options);
|
145 | const isValidType = [((typeDetails.customType || {}).functions || {}).isOfType, typeDetails.isOfType].filter((a) => Boolean(a)).some((func) => func(value, settings.type));
|
146 | return {typeDetails, isValidType};
|
147 | };
|
148 | const checkTypeFunction = (item) => {
|
149 | const [key, value] = item;
|
150 | if (validParents.find((parent) => key.startsWith(parent.key) && (parent.infinite || key.split(".").length === parent.key.split(".").length + 1))) {
|
151 | return;
|
152 | }
|
153 | const genericKey = key.replace(/\.\d+/gu, ".0");
|
154 | const existsInSchema = schemaAttributes.includes(genericKey);
|
155 | if (existsInSchema) {
|
156 | const {isValidType, typeDetails} = getValueTypeCheckResult(value, genericKey, {"standardKey": true});
|
157 | if (!isValidType) {
|
158 | throw new Error.TypeMismatch(`Expected ${key} to be of type ${typeDetails.name.toLowerCase()}, instead found type ${typeof value}.`);
|
159 | } else if (typeDetails.isSet) {
|
160 | validParents.push({key, "infinite": true});
|
161 | } else if (typeDetails.dynamodbType === "L") {
|
162 |
|
163 | value.forEach((subValue, index, array) => {
|
164 | if (index === 0 || typeof subValue !== typeof array[0]) {
|
165 | checkTypeFunction([`${key}.${index}`, subValue]);
|
166 | }
|
167 | });
|
168 | validParents.push({key});
|
169 | }
|
170 | } else {
|
171 |
|
172 | if (!settings.saveUnknown || !utils.dynamoose.wildcard_allowed_check(model.schema.getSettingValue("saveUnknown"), key)) {
|
173 | keysToDelete.push(key);
|
174 | }
|
175 | }
|
176 | };
|
177 | utils.object.entries(returnObject).filter((item) => item[1] !== undefined && item[1] !== dynamooseUndefined).map(checkTypeFunction);
|
178 | keysToDelete.reverse().forEach((key) => utils.object.delete(returnObject, key));
|
179 |
|
180 | if (settings.defaults || settings.forceDefault) {
|
181 | await Promise.all(Document.attributesWithSchema(returnObject).map(async (key) => {
|
182 | const value = utils.object.get(returnObject, key);
|
183 | if (value === dynamooseUndefined) {
|
184 | utils.object.set(returnObject, key, undefined);
|
185 | } else {
|
186 | const defaultValue = await model.schema.defaultCheck(key, value, settings);
|
187 | const isDefaultValueUndefined = typeof defaultValue === "undefined" || defaultValue === null;
|
188 | if (!isDefaultValueUndefined) {
|
189 | const {isValidType, typeDetails} = getValueTypeCheckResult(defaultValue, key);
|
190 | if (!isValidType) {
|
191 | throw new Error.TypeMismatch(`Expected ${key} to be of type ${typeDetails.name.toLowerCase()}, instead found type ${typeof defaultValue}.`);
|
192 | } else {
|
193 | utils.object.set(returnObject, key, defaultValue);
|
194 | }
|
195 | }
|
196 | }
|
197 | }));
|
198 | }
|
199 |
|
200 | if (settings.customTypesDynamo) {
|
201 | Document.attributesWithSchema(returnObject).map((key) => {
|
202 | const value = utils.object.get(returnObject, key);
|
203 | const isValueUndefined = typeof value === "undefined" || value === null;
|
204 | if (!isValueUndefined) {
|
205 | const typeDetails = model.schema.getAttributeTypeDetails(key);
|
206 | const {customType} = typeDetails;
|
207 | const {type: typeInfo} = typeDetails.isOfType(value);
|
208 | const isCorrectTypeAlready = typeInfo === (settings.type === "toDynamo" ? "underlying" : "main");
|
209 | if (customType && !isCorrectTypeAlready) {
|
210 | const customValue = customType.functions[settings.type](value);
|
211 | utils.object.set(returnObject, key, customValue);
|
212 | }
|
213 | }
|
214 | });
|
215 | }
|
216 |
|
217 | utils.object.entries(returnObject).filter((item) => typeof item[1] === "object").forEach((item) => {
|
218 | const [key, value] = item;
|
219 | let typeDetails;
|
220 | try {
|
221 | typeDetails = model.schema.getAttributeTypeDetails(key);
|
222 | } catch (e) {
|
223 | const Schema = require("./Schema");
|
224 | typeDetails = Schema.attributeTypes.findTypeForValue(value, settings.type, settings);
|
225 | }
|
226 |
|
227 | if (typeDetails && typeDetails[settings.type]) {
|
228 | utils.object.set(returnObject, key, typeDetails[settings.type](value));
|
229 | }
|
230 | });
|
231 | if (settings.modifiers) {
|
232 | await Promise.all(settings.modifiers.map((modifier) => {
|
233 | return Promise.all(Document.attributesWithSchema(returnObject).map(async (key) => {
|
234 | const value = utils.object.get(returnObject, key);
|
235 | const modifierFunction = await model.schema.getAttributeSettingValue(modifier, key, {"returnFunction": true});
|
236 | const isValueUndefined = typeof value === "undefined" || value === null;
|
237 | if (modifierFunction && !isValueUndefined) {
|
238 | utils.object.set(returnObject, key, await modifierFunction(value));
|
239 | }
|
240 | }));
|
241 | }));
|
242 | }
|
243 | if (settings.validate) {
|
244 | await Promise.all(Document.attributesWithSchema(returnObject).map(async (key) => {
|
245 | const value = utils.object.get(returnObject, key);
|
246 | const isValueUndefined = typeof value === "undefined" || value === null;
|
247 | if (!isValueUndefined) {
|
248 | const validator = await model.schema.getAttributeSettingValue("validate", key, {"returnFunction": true});
|
249 | if (validator) {
|
250 | let result;
|
251 | if (validator instanceof RegExp) {
|
252 | result = validator.test(value);
|
253 | } else {
|
254 | result = typeof validator === "function" ? await validator(value) : validator === value;
|
255 | }
|
256 |
|
257 | if (!result) {
|
258 | throw new Error.ValidationError(`${key} with a value of ${value} had a validation error when trying to save the document`);
|
259 | }
|
260 | }
|
261 | }
|
262 | }));
|
263 | }
|
264 | if (settings.required) {
|
265 | let attributesToCheck = Document.attributesWithSchema(returnObject);
|
266 | if (settings.required === "nested") {
|
267 | attributesToCheck = attributesToCheck.filter((attribute) => utils.object.keys(returnObject).find((key) => attribute.startsWith(key)));
|
268 | }
|
269 | await Promise.all(attributesToCheck.map(async (key) => {
|
270 | async function check() {
|
271 | const value = utils.object.get(returnObject, key);
|
272 | await model.schema.requiredCheck(key, value);
|
273 | }
|
274 |
|
275 | const keyParts = key.split(".");
|
276 | const parentKey = keyParts.slice(0, -1).join(".");
|
277 | if (parentKey) {
|
278 | const parentValue = utils.object.get(returnObject, parentKey);
|
279 | const isParentValueUndefined = typeof parentValue === "undefined" || parentValue === null;
|
280 | if (!isParentValueUndefined) {
|
281 | await check();
|
282 | }
|
283 | } else {
|
284 | await check();
|
285 | }
|
286 | }));
|
287 | }
|
288 | if (settings.enum) {
|
289 | await Promise.all(Document.attributesWithSchema(returnObject).map(async (key) => {
|
290 | const value = utils.object.get(returnObject, key);
|
291 | const isValueUndefined = typeof value === "undefined" || value === null;
|
292 | if (!isValueUndefined) {
|
293 | const enumArray = await model.schema.getAttributeSettingValue("enum", key);
|
294 | if (enumArray && !enumArray.includes(value)) {
|
295 | throw new Error.ValidationError(`${key} must equal ${JSON.stringify(enumArray)}, but is set to ${value}`);
|
296 | }
|
297 | }
|
298 | }));
|
299 | }
|
300 |
|
301 | return returnObject;
|
302 | };
|
303 | Document.prototype.toDynamo = async function(settings = {}) {
|
304 | settings.type = "toDynamo";
|
305 | Document.prepareForObjectFromSchema(this, settings);
|
306 | const object = await Document.objectFromSchema(this, settings);
|
307 | return Document.toDynamo(object);
|
308 | };
|
309 | Document.prototype.save = function(settings = {}, callback) {
|
310 | if (typeof settings === "function" && !callback) {
|
311 | callback = settings;
|
312 | settings = {};
|
313 | }
|
314 |
|
315 | const paramsPromise = this.toDynamo({"defaults": true, "validate": true, "required": true, "enum": true, "forceDefault": true, "saveUnknown": true, "customTypesDynamo": true, "updateTimestamps": true, "modifiers": ["set"]}).then((item) => {
|
316 | const putItemObj = {
|
317 | "Item": item,
|
318 | "TableName": Document.Model.name
|
319 | };
|
320 |
|
321 | if (settings.overwrite === false) {
|
322 | putItemObj.ConditionExpression = "attribute_not_exists(#__hash_key)";
|
323 | putItemObj.ExpressionAttributeNames = {"#__hash_key": model.schema.getHashKey()};
|
324 | }
|
325 |
|
326 | return putItemObj;
|
327 | });
|
328 | if (settings.return === "request") {
|
329 | if (callback) {
|
330 | paramsPromise.then((result) => callback(null, result));
|
331 | return;
|
332 | } else {
|
333 | return paramsPromise;
|
334 | }
|
335 | }
|
336 | const promise = Promise.all([paramsPromise, Document.Model.pendingTaskPromise()]).then((promises) => {
|
337 | const [putItemObj] = promises;
|
338 | return ddb("putItem", putItemObj);
|
339 | });
|
340 |
|
341 | if (callback) {
|
342 | promise.then(() => {this[internalProperties].storedInDynamo = true; return callback(null, this);}).catch((error) => callback(error));
|
343 | } else {
|
344 | return (async () => {
|
345 | await promise;
|
346 | this[internalProperties].storedInDynamo = true;
|
347 | return this;
|
348 | })();
|
349 | }
|
350 | };
|
351 | Document.prototype.delete = function(callback) {
|
352 | return model.delete({
|
353 | [model.schema.getHashKey()]: this[model.schema.getHashKey()]
|
354 | }, callback);
|
355 | };
|
356 |
|
357 | Document.prototype.conformToSchema = async function(settings = {"type": "fromDynamo"}) {
|
358 | Document.prepareForObjectFromSchema(this, settings);
|
359 | const expectedObject = await Document.objectFromSchema(this, settings);
|
360 | if (!expectedObject) {
|
361 | return expectedObject;
|
362 | }
|
363 | const expectedKeys = Object.keys(expectedObject);
|
364 | Object.keys(this).forEach((key) => {
|
365 | if (!expectedKeys.includes(key)) {
|
366 | delete this[key];
|
367 | } else if (this[key] !== expectedObject[key]) {
|
368 | this[key] = expectedObject[key];
|
369 | }
|
370 | });
|
371 |
|
372 | return this;
|
373 | };
|
374 |
|
375 | Document.prototype.original = function() {
|
376 | return this[internalProperties].originalSettings.type === "fromDynamo" ? this[internalProperties].originalObject : null;
|
377 | };
|
378 |
|
379 | Document.Model = model;
|
380 |
|
381 | return Document;
|
382 | }
|
383 |
|
384 | applyStaticMethods(DocumentCarrier);
|
385 |
|
386 | module.exports = DocumentCarrier;
|