UNPKG

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