1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.AnyDocument = exports.Document = void 0;
|
4 | const aws = require("./aws");
|
5 | const ddb = require("./aws/ddb/internal");
|
6 | const utils = require("./utils");
|
7 | const Error = require("./Error");
|
8 | const Internal = require("./Internal");
|
9 | const { internalProperties } = Internal.General;
|
10 | const dynamooseUndefined = Internal.Public.undefined;
|
11 | const Populate_1 = require("./Populate");
|
12 |
|
13 | class 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 |
|
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 |
|
60 | async prepareForResponse() {
|
61 | if (this.model.options.populate) {
|
62 | return this.populate({ "properties": this.model.options.populate });
|
63 | }
|
64 | return this;
|
65 | }
|
66 |
|
67 | original() {
|
68 | return this[internalProperties].originalSettings.type === "fromDynamo" ? this[internalProperties].originalObject : null;
|
69 |
|
70 | }
|
71 | toJSON() {
|
72 | return utils.dynamoose.documentToJSON.bind(this)();
|
73 | }
|
74 |
|
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 | }
|
144 | exports.Document = Document;
|
145 | class AnyDocument extends Document {
|
146 | }
|
147 | exports.AnyDocument = AnyDocument;
|
148 |
|
149 | Document.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 |
|
171 |
|
172 | const attributesWithSchemaCache = {};
|
173 | Document.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 |
|
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 |
|
190 | function traverse(node, treeNode, outPath, callback) {
|
191 | callback(outPath);
|
192 | if (Object.keys(treeNode).length === 0) {
|
193 | return;
|
194 | }
|
195 | Object.keys(treeNode).forEach((attr) => {
|
196 | if (attr === "0") {
|
197 |
|
198 | if (!node || node.length == 0 || typeof node === "object" && Object.keys(node).length == 0) {
|
199 | node = [{}];
|
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 = {};
|
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 |
|
224 | Document.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 |
|
232 | const validParents = [];
|
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");
|
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 ( matchedTypeDetails.dynamodbType === "L") {
|
251 |
|
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 |
|
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 |
|
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 |
|
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) { }
|
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 |
|
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 | };
|
422 | Document.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 |
|
429 | Document.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 |
|
\ | No newline at end of file |