UNPKG

23.7 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.AnyDocument = exports.Document = void 0;
4const aws = require("./aws");
5const ddb = require("./aws/ddb/internal");
6const utils = require("./utils");
7const Error = require("./Error");
8const Internal = require("./Internal");
9const { internalProperties } = Internal.General;
10const dynamooseUndefined = Internal.Public.undefined;
11const Populate_1 = require("./Populate");
12// Document represents an item in a Model that is either pending (not saved) or saved
13class Document {
14 constructor(model, object, settings) {
15 const documentObject = Document.isDynamoObject(object) ? aws.converter().unmarshall(object) : object;
16 Object.keys(documentObject).forEach((key) => this[key] = documentObject[key]);
17 Object.defineProperty(this, internalProperties, {
18 "configurable": false,
19 "value": {}
20 });
21 this[internalProperties].originalObject = JSON.parse(JSON.stringify(documentObject));
22 this[internalProperties].originalSettings = Object.assign({}, settings);
23 Object.defineProperty(this, "model", {
24 "configurable": false,
25 "value": model
26 });
27 if (settings.type === "fromDynamo") {
28 this[internalProperties].storedInDynamo = true;
29 }
30 }
31 static objectToDynamo(object, settings = { "type": "object" }) {
32 return (settings.type === "value" ? aws.converter().input : aws.converter().marshall)(object);
33 }
34 static fromDynamo(object) {
35 return aws.converter().unmarshall(object);
36 }
37 // This function will return null if it's unknown if it is a Dynamo object (ex. empty object). It will return true if it is a Dynamo object and false if it's not.
38 static isDynamoObject(object, recurrsive) {
39 function isValid(value) {
40 if (typeof value === "undefined" || value === null) {
41 return false;
42 }
43 const keys = Object.keys(value);
44 const key = keys[0];
45 const nestedResult = typeof value[key] === "object" && !(value[key] instanceof Buffer) ? Array.isArray(value[key]) ? value[key].every((value) => Document.isDynamoObject(value, true)) : Document.isDynamoObject(value[key]) : true;
46 const { Schema } = require("./Schema");
47 const attributeType = Schema.attributeTypes.findDynamoDBType(key);
48 return typeof value === "object" && keys.length === 1 && attributeType && (nestedResult || Object.keys(value[key]).length === 0 || attributeType.isSet);
49 }
50 const keys = Object.keys(object);
51 const values = Object.values(object);
52 if (keys.length === 0) {
53 return null;
54 }
55 else {
56 return recurrsive ? isValid(object) : values.every((value) => isValid(value));
57 }
58 }
59 // This function handles actions that should take place before every response (get, scan, query, batchGet, etc.)
60 async prepareForResponse() {
61 if (this.model.options.populate) {
62 return this.populate({ "properties": this.model.options.populate });
63 }
64 return this;
65 }
66 // Original
67 original() {
68 return this[internalProperties].originalSettings.type === "fromDynamo" ? this[internalProperties].originalObject : null;
69 // toJSON
70 }
71 toJSON() {
72 return utils.dynamoose.documentToJSON.bind(this)();
73 }
74 // Serializer
75 serialize(nameOrOptions) {
76 return this.model.serializer._serialize(this, nameOrOptions);
77 }
78 delete(callback) {
79 const hashKey = this.model.getHashKey();
80 const rangeKey = this.model.getRangeKey();
81 const key = { [hashKey]: this[hashKey] };
82 if (rangeKey) {
83 key[rangeKey] = this[rangeKey];
84 }
85 return this.model.delete(key, callback);
86 }
87 save(settings, callback) {
88 if (typeof settings !== "object" && typeof settings !== "undefined") {
89 callback = settings;
90 settings = {};
91 }
92 if (typeof settings === "undefined") {
93 settings = {};
94 }
95 const localSettings = settings;
96 const paramsPromise = this.toDynamo({ "defaults": true, "validate": true, "required": true, "enum": true, "forceDefault": true, "combine": true, "saveUnknown": true, "customTypesDynamo": true, "updateTimestamps": true, "modifiers": ["set"] }).then((item) => {
97 let putItemObj = {
98 "Item": item,
99 "TableName": this.model.name
100 };
101 if (localSettings.condition) {
102 putItemObj = Object.assign(Object.assign({}, putItemObj), localSettings.condition.requestObject());
103 }
104 if (localSettings.overwrite === false) {
105 const conditionExpression = "attribute_not_exists(#__hash_key)";
106 putItemObj.ConditionExpression = putItemObj.ConditionExpression ? `(${putItemObj.ConditionExpression}) AND (${conditionExpression})` : conditionExpression;
107 putItemObj.ExpressionAttributeNames = Object.assign(Object.assign({}, putItemObj.ExpressionAttributeNames || {}), { "#__hash_key": this.model.getHashKey() });
108 }
109 return putItemObj;
110 });
111 if (settings.return === "request") {
112 if (callback) {
113 const localCallback = callback;
114 paramsPromise.then((result) => localCallback(null, result));
115 return;
116 }
117 else {
118 return paramsPromise;
119 }
120 }
121 const promise = Promise.all([paramsPromise, this.model.pendingTaskPromise()]).then((promises) => {
122 const [putItemObj] = promises;
123 return ddb("putItem", putItemObj);
124 });
125 if (callback) {
126 const localCallback = callback;
127 promise.then(() => {
128 this[internalProperties].storedInDynamo = true;
129 localCallback(null, this);
130 }).catch((error) => callback(error));
131 }
132 else {
133 return (async () => {
134 await promise;
135 this[internalProperties].storedInDynamo = true;
136 return this;
137 })();
138 }
139 }
140 populate(...args) {
141 return Populate_1.PopulateDocument.bind(this)(...args);
142 }
143}
144exports.Document = Document;
145class AnyDocument extends Document {
146}
147exports.AnyDocument = AnyDocument;
148// This function will mutate the object passed in to run any actions to conform to the schema that cannot be achieved through non mutating methods in Document.objectFromSchema (setting timestamps, etc.)
149Document.prepareForObjectFromSchema = async function (object, model, settings) {
150 if (settings.updateTimestamps) {
151 const schema = await model.schemaForObject(object);
152 if (schema.settings.timestamps && settings.type === "toDynamo") {
153 const date = new Date();
154 const createdAtProperties = (Array.isArray(schema.settings.timestamps.createdAt) ? schema.settings.timestamps.createdAt : [schema.settings.timestamps.createdAt]).filter((a) => Boolean(a));
155 const updatedAtProperties = (Array.isArray(schema.settings.timestamps.updatedAt) ? schema.settings.timestamps.updatedAt : [schema.settings.timestamps.updatedAt]).filter((a) => Boolean(a));
156 if (object[internalProperties] && !object[internalProperties].storedInDynamo && (typeof settings.updateTimestamps === "boolean" || settings.updateTimestamps.createdAt)) {
157 createdAtProperties.forEach((prop) => {
158 utils.object.set(object, prop, date);
159 });
160 }
161 if (typeof settings.updateTimestamps === "boolean" || settings.updateTimestamps.updatedAt) {
162 updatedAtProperties.forEach((prop) => {
163 utils.object.set(object, prop, date);
164 });
165 }
166 }
167 }
168 return object;
169};
170// This function will return a list of attributes combining both the schema attributes with the document attributes. This also takes into account all attributes that could exist (ex. properties in sets that don't exist in document), adding the indexes for each item in the document set.
171// https://stackoverflow.com/a/59928314/894067
172const attributesWithSchemaCache = {};
173Document.attributesWithSchema = async function (document, model) {
174 const schema = await model.schemaForObject(document);
175 const attributes = schema.attributes();
176 const documentID = utils.object.keys(document).join("");
177 if (attributesWithSchemaCache[documentID] && attributesWithSchemaCache[documentID][attributes.join()]) {
178 return attributesWithSchemaCache[documentID][attributes.join()];
179 }
180 // build a tree out of schema attributes
181 const root = {};
182 attributes.forEach((attribute) => {
183 let node = root;
184 attribute.split(".").forEach((part) => {
185 node[part] = node[part] || {};
186 node = node[part];
187 });
188 });
189 // explore the tree
190 function traverse(node, treeNode, outPath, callback) {
191 callback(outPath);
192 if (Object.keys(treeNode).length === 0) { // a leaf
193 return;
194 }
195 Object.keys(treeNode).forEach((attr) => {
196 if (attr === "0") {
197 // We check for empty objects here (added `typeof node === "object" && Object.keys(node).length == 0`, see PR https://github.com/dynamoose/dynamoose/pull/1034) to handle the use case of 2d arrays, or arrays within arrays. `node` in that case will be an empty object.
198 if (!node || node.length == 0 || typeof node === "object" && Object.keys(node).length == 0) {
199 node = [{}]; // fake the path for arrays
200 }
201 node.forEach((a, index) => {
202 outPath.push(index);
203 traverse(node[index], treeNode[attr], outPath, callback);
204 outPath.pop();
205 });
206 }
207 else {
208 if (!node) {
209 node = {}; // fake the path for properties
210 }
211 outPath.push(attr);
212 traverse(node[attr], treeNode[attr], outPath, callback);
213 outPath.pop();
214 }
215 });
216 }
217 const out = [];
218 traverse(document, root, [], (val) => out.push(val.join(".")));
219 const result = out.slice(1);
220 attributesWithSchemaCache[documentID] = { [attributes.join()]: result };
221 return result;
222};
223// This function will return an object that conforms to the schema (removing any properties that don't exist, using default values, etc.) & throws an error if there is a typemismatch.
224Document.objectFromSchema = async function (object, model, settings = { "type": "toDynamo" }) {
225 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()) {
226 return undefined;
227 }
228 const returnObject = Object.assign({}, object);
229 const schema = settings.schema || await model.schemaForObject(returnObject);
230 const schemaAttributes = schema.attributes(returnObject);
231 // Type check
232 const validParents = []; // This array is used to allow for set contents to not be type checked
233 const keysToDelete = [];
234 const typeIndexOptionMap = schema.getTypePaths(returnObject, settings);
235 const checkTypeFunction = (item) => {
236 const [key, value] = item;
237 if (validParents.find((parent) => key.startsWith(parent.key) && (parent.infinite || key.split(".").length === parent.key.split(".").length + 1))) {
238 return;
239 }
240 const genericKey = key.replace(/\.\d+/gu, ".0"); // This is a key replacing all list numbers with 0 to standardize things like checking if it exists in the schema
241 const existsInSchema = schemaAttributes.includes(genericKey);
242 if (existsInSchema) {
243 const { isValidType, matchedTypeDetails, typeDetailsArray } = utils.dynamoose.getValueTypeCheckResult(schema, value, genericKey, settings, { "standardKey": true, typeIndexOptionMap });
244 if (!isValidType) {
245 throw new Error.TypeMismatch(`Expected ${key} to be of type ${typeDetailsArray.map((detail) => detail.dynamicName ? detail.dynamicName() : detail.name.toLowerCase()).join(", ")}, instead found type ${typeof value}${typeDetailsArray.some((val) => val.name === "Constant") ? ` (${value})` : ""}.`);
246 }
247 else if (matchedTypeDetails.isSet || matchedTypeDetails.name.toLowerCase() === "model") {
248 validParents.push({ key, "infinite": true });
249 }
250 else if ( /*typeDetails.dynamodbType === "M" || */matchedTypeDetails.dynamodbType === "L") {
251 // The code below is an optimization for large array types to speed up the process of not having to check the type for every element but only the ones that are different
252 value.forEach((subValue, index, array) => {
253 if (index === 0 || typeof subValue !== typeof array[0]) {
254 checkTypeFunction([`${key}.${index}`, subValue]);
255 }
256 else if (keysToDelete.includes(`${key}.0`) && typeof subValue === typeof array[0]) {
257 keysToDelete.push(`${key}.${index}`);
258 }
259 });
260 validParents.push({ key });
261 }
262 }
263 else {
264 // Check saveUnknown
265 if (!settings.saveUnknown || !utils.dynamoose.wildcard_allowed_check(schema.getSettingValue("saveUnknown"), key)) {
266 keysToDelete.push(key);
267 }
268 }
269 };
270 utils.object.entries(returnObject).filter((item) => item[1] !== undefined && item[1] !== dynamooseUndefined).map(checkTypeFunction);
271 keysToDelete.reverse().forEach((key) => utils.object.delete(returnObject, key));
272 if (settings.defaults || settings.forceDefault) {
273 await Promise.all((await Document.attributesWithSchema(returnObject, model)).map(async (key) => {
274 const value = utils.object.get(returnObject, key);
275 if (value === dynamooseUndefined) {
276 utils.object.set(returnObject, key, undefined);
277 }
278 else {
279 const defaultValue = await schema.defaultCheck(key, value, settings);
280 const isDefaultValueUndefined = Array.isArray(defaultValue) ? defaultValue.some((defaultValue) => typeof defaultValue === "undefined" || defaultValue === null) : typeof defaultValue === "undefined" || defaultValue === null;
281 if (!isDefaultValueUndefined) {
282 const { isValidType, typeDetailsArray } = utils.dynamoose.getValueTypeCheckResult(schema, defaultValue, key, settings, { typeIndexOptionMap });
283 if (!isValidType) {
284 throw new Error.TypeMismatch(`Expected ${key} to be of type ${typeDetailsArray.map((detail) => detail.dynamicName ? detail.dynamicName() : detail.name.toLowerCase()).join(", ")}, instead found type ${typeof defaultValue}.`);
285 }
286 else {
287 utils.object.set(returnObject, key, defaultValue);
288 }
289 }
290 }
291 }));
292 }
293 // Custom Types
294 if (settings.customTypesDynamo) {
295 (await Document.attributesWithSchema(returnObject, model)).map((key) => {
296 const value = utils.object.get(returnObject, key);
297 const isValueUndefined = typeof value === "undefined" || value === null;
298 if (!isValueUndefined) {
299 const typeDetails = utils.dynamoose.getValueTypeCheckResult(schema, value, key, settings, { typeIndexOptionMap }).matchedTypeDetails;
300 const { customType } = typeDetails;
301 const { "type": typeInfo } = typeDetails.isOfType(value);
302 const isCorrectTypeAlready = typeInfo === (settings.type === "toDynamo" ? "underlying" : "main");
303 if (customType && customType.functions[settings.type] && !isCorrectTypeAlready) {
304 const customValue = customType.functions[settings.type](value);
305 utils.object.set(returnObject, key, customValue);
306 }
307 }
308 });
309 }
310 // DynamoDB Type Handler (ex. converting sets to correct value for toDynamo & fromDynamo)
311 utils.object.entries(returnObject).filter((item) => typeof item[1] === "object").forEach((item) => {
312 const [key, value] = item;
313 let typeDetails;
314 try {
315 typeDetails = utils.dynamoose.getValueTypeCheckResult(schema, value, key, settings, { typeIndexOptionMap }).matchedTypeDetails;
316 }
317 catch (e) {
318 const { Schema } = require("./Schema");
319 typeDetails = Schema.attributeTypes.findTypeForValue(value, settings.type, settings);
320 }
321 if (typeDetails && typeDetails[settings.type]) {
322 utils.object.set(returnObject, key, typeDetails[settings.type](value));
323 }
324 });
325 if (settings.combine) {
326 schemaAttributes.map((key) => {
327 try {
328 const typeDetails = schema.getAttributeTypeDetails(key);
329 return {
330 key,
331 "type": typeDetails
332 };
333 }
334 catch (e) { } // eslint-disable-line no-empty
335 }).filter((item) => {
336 return Array.isArray(item.type) ? item.type.some((type) => type.name === "Combine") : item.type.name === "Combine";
337 }).map((obj) => {
338 if (obj && Array.isArray(obj.type)) {
339 throw new Error.InvalidParameter("Combine type is not allowed to be used with multiple types.");
340 }
341 return obj;
342 }).forEach((item) => {
343 const { key, type } = item;
344 const value = type.typeSettings.attributes.map((attribute) => utils.object.get(returnObject, attribute)).filter((value) => typeof value !== "undefined" && value !== null).join(type.typeSettings.seperator);
345 utils.object.set(returnObject, key, value);
346 });
347 }
348 if (settings.modifiers) {
349 await Promise.all(settings.modifiers.map(async (modifier) => {
350 return Promise.all((await Document.attributesWithSchema(returnObject, model)).map(async (key) => {
351 const value = utils.object.get(returnObject, key);
352 const modifierFunction = await schema.getAttributeSettingValue(modifier, key, { "returnFunction": true, typeIndexOptionMap });
353 const modifierFunctionExists = Array.isArray(modifierFunction) ? modifierFunction.some((val) => Boolean(val)) : Boolean(modifierFunction);
354 const isValueUndefined = typeof value === "undefined" || value === null;
355 if (modifierFunctionExists && !isValueUndefined) {
356 const oldValue = object.original ? utils.object.get(object.original(), key) : undefined;
357 utils.object.set(returnObject, key, await modifierFunction(value, oldValue));
358 }
359 }));
360 }));
361 }
362 if (settings.validate) {
363 await Promise.all((await Document.attributesWithSchema(returnObject, model)).map(async (key) => {
364 const value = utils.object.get(returnObject, key);
365 const isValueUndefined = typeof value === "undefined" || value === null;
366 if (!isValueUndefined) {
367 const validator = await schema.getAttributeSettingValue("validate", key, { "returnFunction": true, typeIndexOptionMap });
368 if (validator) {
369 let result;
370 if (validator instanceof RegExp) {
371 // TODO: fix the line below to not use `as`. This will cause a weird issue even in vanilla JS, where if your validator is a Regular Expression but the type isn't a string, it will throw a super random error.
372 result = validator.test(value);
373 }
374 else {
375 result = typeof validator === "function" ? await validator(value) : validator === value;
376 }
377 if (!result) {
378 throw new Error.ValidationError(`${key} with a value of ${value} had a validation error when trying to save the document`);
379 }
380 }
381 }
382 }));
383 }
384 if (settings.required) {
385 let attributesToCheck = await Document.attributesWithSchema(returnObject, model);
386 if (settings.required === "nested") {
387 attributesToCheck = attributesToCheck.filter((attribute) => utils.object.keys(returnObject).find((key) => attribute.startsWith(key)));
388 }
389 await Promise.all(attributesToCheck.map(async (key) => {
390 const check = async () => {
391 const value = utils.object.get(returnObject, key);
392 await schema.requiredCheck(key, value);
393 };
394 const keyParts = key.split(".");
395 const parentKey = keyParts.slice(0, -1).join(".");
396 if (parentKey) {
397 const parentValue = utils.object.get(returnObject, parentKey);
398 const isParentValueUndefined = typeof parentValue === "undefined" || parentValue === null;
399 if (!isParentValueUndefined) {
400 await check();
401 }
402 }
403 else {
404 await check();
405 }
406 }));
407 }
408 if (settings.enum) {
409 await Promise.all((await Document.attributesWithSchema(returnObject, model)).map(async (key) => {
410 const value = utils.object.get(returnObject, key);
411 const isValueUndefined = typeof value === "undefined" || value === null;
412 if (!isValueUndefined) {
413 const enumArray = await schema.getAttributeSettingValue("enum", key, { "returnFunction": false, typeIndexOptionMap });
414 if (enumArray && !enumArray.includes(value)) {
415 throw new Error.ValidationError(`${key} must equal ${JSON.stringify(enumArray)}, but is set to ${value}`);
416 }
417 }
418 }));
419 }
420 return returnObject;
421};
422Document.prototype.toDynamo = async function (settings = {}) {
423 const newSettings = Object.assign(Object.assign({}, settings), { "type": "toDynamo" });
424 await Document.prepareForObjectFromSchema(this, this.model, newSettings);
425 const object = await Document.objectFromSchema(this, this.model, newSettings);
426 return Document.objectToDynamo(object);
427};
428// This function will modify the document to conform to the Schema
429Document.prototype.conformToSchema = async function (settings = { "type": "fromDynamo" }) {
430 let document = this;
431 if (settings.type === "fromDynamo") {
432 document = await this.prepareForResponse();
433 }
434 await Document.prepareForObjectFromSchema(document, document.model, settings);
435 const expectedObject = await Document.objectFromSchema(document, document.model, settings);
436 if (!expectedObject) {
437 return expectedObject;
438 }
439 const expectedKeys = Object.keys(expectedObject);
440 Object.keys(document).forEach((key) => {
441 if (!expectedKeys.includes(key)) {
442 delete this[key];
443 }
444 else if (this[key] !== expectedObject[key]) {
445 this[key] = expectedObject[key];
446 }
447 });
448 return this;
449};
450//# sourceMappingURL=Document.js.map
\No newline at end of file