ottoman
Version:
Ottoman Couchbase ODM
627 lines • 22.7 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _Document_isNew;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Document = void 0;
const couchbase_1 = require("couchbase");
const ottoman_errors_1 = require("../exceptions/ottoman-errors");
const schema_1 = require("../schema");
const cast_strategy_1 = require("../utils/cast-strategy");
const constants_1 = require("../utils/constants");
const extract_data_from_model_1 = require("../utils/extract-data-from-model");
const generate_uuid_1 = require("../utils/generate-uuid");
const schema_utils_1 = require("../utils/schema.utils");
const array_diff_1 = require("./utils/array-diff");
const get_model_ref_keys_1 = require("./utils/get-model-ref-keys");
const model_utils_1 = require("./utils/model.utils");
const remove_life_cycle_1 = require("./utils/remove-life-cycle");
const store_life_cycle_1 = require("./utils/store-life-cycle");
const utils_1 = require("../utils");
const merge_1 = require("../utils/merge");
/**
* Document class represents a database document and provides useful methods to work with.
*
* @example
* ```javascript
* import { connect, model } from "ottoman";
* connect("couchbase://localhost/travel-sample@admin:password");
*
* // Create an `User` model
* const User = model('User', { name: String });
*
* // Create a document from the `User` Model
* const jane = new User({ name: "Jane Doe" })
* ```
*/
class Document {
/**
* @ignore
*/
// eslint-disable-next-line no-unused-vars
constructor(data, options = {}) {
/**
* @ignore
*/
_Document_isNew.set(this, true);
const { get, set, getStrategy, setStrategy, hasOwnProperty } = (function immutables() {
const i = {};
let strategy;
return {
get(key) {
return i[key];
},
set(key, value) {
i[key] = value;
},
hasOwnProperty(key) {
return i.hasOwnProperty(key);
},
getStrategy() {
return strategy;
},
setStrategy(value) {
strategy = value;
},
};
})();
Object.defineProperties(this, {
setImmutable: {
value: function (key, value) {
set(key, value);
},
},
getImmutable: {
value: function (key) {
return get(key);
},
},
immutableHasOwnProperty: {
value: function (key) {
return hasOwnProperty(key);
},
},
getCurrentStrategy: {
value: getStrategy,
},
setCurrentStrategy: {
value: function (value) {
setStrategy(value);
},
},
$wasNew: {
value: () => {
__classPrivateFieldSet(this, _Document_isNew, false, "f");
return this;
},
},
});
}
/**
* @ignore
*/
get $() {
return (0, model_utils_1.getModelMetadata)(this.constructor);
}
/**
* Returns id value, useful when working with dynamic ID_KEY.
*
* @example
* ```javascript
* console.log(user._getId()); // 'userId'
* console.log(user.id); // 'userId'
* ```
*/
_getId() {
return this[this.$.ID_KEY];
}
/**
* Returns id key.
*/
_getIdField() {
return this.$.ID_KEY;
}
/**
* Saves or Updates the document.
*
* @example
* ```javascript
* const user = new User({ name: "John Doe" }); //user document created, it's not saved yet
*
* await user.save(); // user saved into the DB
*
* // You also can force save function to only create new Documents by passing true as argument
* await user.save(true); // ensure to execute a insert operation
* ```
*/
save(onlyCreate = false, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
const { scopeName, collectionName, modelKey, collection, keyGenerator, modelName, ottoman, keyGeneratorDelimiter } = this.$;
const data = (0, extract_data_from_model_1.extractDataFromModel)(this);
const _options = options;
let id = this._getId();
const prefix = `${scopeName}${collectionName}`;
const newRefKeys = (0, get_model_ref_keys_1.getModelRefKeys)(data, prefix, ottoman);
const refKeys = {
add: [],
remove: [],
};
const metadata = this.$;
let key = '';
if (!id) {
id = (0, generate_uuid_1.generateUUID)();
key = constants_1._keyGenerator(keyGenerator, { metadata, id }, keyGeneratorDelimiter);
if (!data[this._getIdField()]) {
data[this._getIdField()] = id;
}
refKeys.add = newRefKeys;
}
else {
try {
key = constants_1._keyGenerator(keyGenerator, { metadata, id }, keyGeneratorDelimiter);
const { cas, value: oldData } = yield (options.transactionContext
? options.transactionContext.get(collection(), key)
: collection().get(key));
if (cas && onlyCreate) {
throw new couchbase_1.DocumentExistsError();
}
const oldRefKeys = (0, get_model_ref_keys_1.getModelRefKeys)(oldData, prefix, ottoman);
refKeys.add = (0, array_diff_1.arrayDiff)(newRefKeys, oldRefKeys);
refKeys.remove = (0, array_diff_1.arrayDiff)(oldRefKeys, newRefKeys);
if (cas) {
_options.cas = cas;
}
}
catch (e) {
if (e instanceof couchbase_1.DocumentNotFoundError) {
refKeys.add = newRefKeys;
}
}
}
const modelKeyObj = {};
(0, utils_1.setValueByPath)(modelKeyObj, modelKey, modelName);
const addedMetadata = (0, merge_1.mergeDoc)(data, modelKeyObj);
const { document } = yield (0, store_life_cycle_1.storeLifeCycle)({ key, id, data: addedMetadata, options: _options, metadata, refKeys });
return this._applyData(document).$wasNew();
});
}
/**
* Removes the document from the database.
*
* @example
* ```javascript
* const user = User.findById('userId')
*
* await user.remove();
* ```
*/
remove(options = {}) {
return __awaiter(this, void 0, void 0, function* () {
const data = (0, extract_data_from_model_1.extractDataFromModel)(this);
const metadata = this.$;
const { keyGenerator, scopeName, collectionName, ottoman, keyGeneratorDelimiter } = metadata;
const prefix = `${scopeName}${collectionName}`;
const refKeys = {
add: [],
remove: (0, get_model_ref_keys_1.getModelRefKeys)(data, prefix, ottoman),
};
const idValue = this._getId();
const id = constants_1._keyGenerator(keyGenerator, { metadata, id: idValue }, keyGeneratorDelimiter);
const { result, document } = yield (0, remove_life_cycle_1.removeLifeCycle)({ id, options, metadata, refKeys, data });
this._applyData(document);
return result;
});
}
/**
* Allows to load document references.
*
*
* @example
* Getting context to explain populate.
* ```typescript
* // Defining the schemas
* const addressSchema = new Schema({
* address: String,
* });
* const personSchema = new Schema({
* name: String,
* age: Number,
* address: { type: addressSchema, ref: 'Address' },
* });
* const companySchema = new Schema({
* president: { type: personSchema, ref: 'Person' },
* ceo: { type: personSchema, ref: 'Person' },
* name: String,
* workers: [{ type: personSchema, ref: 'Person' }],
* });
*
* // Initializing the models
* const Address = model('Address', addressSchema);
* const Person = model('Person', personSchema);
* const Company = model('Company', companySchema);
*
* // Adding some data
* const johnAddress = await Address.create({ address: '13 Washington Square S, New York, NY 10012, USA' });
* const john = await Person.create({ name: 'John Smith', age: 52, address: johnAddress });
*
* const janeAddress = await Address.create({ address: '55 Clark St, Brooklyn, NY 11201, USA' });
* const jane = await Person.create({ name: 'Jane Doe', age: 45, address: janeAddress });
*
* const company = await Company.create({ name: 'Space X', president: john, ceo: jane });
*
*
* // Getting saved company data
* const spaceX = await Company.findById(company.id);
* console.log(`Company: `, company);
* ```
*
* Will get:
* ```
* Company: {
* name: 'Space X',
* president: '50e85ac9-5b4f-4587-aeb6-b287527794c9',
* ceo: '32c2e85a-cc91-4db2-935f-f7d2768168de',
* id: 'd59efbdf-4b7e-4d2f-a986-6e8451f22822',
* _type: 'Company'
* }
* ```
*
* Now we will see how the _populate methods works.
* ```typescript
* const result = await spaceX._populate('ceo');
* console.log(`Result: `, result);
* ```
* ```
* Result: {
* name: 'Space X',
* president: '50e85ac9-5b4f-4587-aeb6-b287527794c9',
* ceo: {
* name: 'Jane Doe',
* age: 45,
* address: '235dd441-b445-4b88-b6aa-2ce35a958a32',
* id: '32c2e85a-cc91-4db2-935f-f7d2768168de',
* _type: 'Person'
* },
* id: 'd59efbdf-4b7e-4d2f-a986-6e8451f22822',
* _type: 'Company'
* }
* ```
*
* Can also pass an array or a string separated by a comma
* ```typescript
* const result = await spaceX._populate('ceo,president');
* // or
* const result = await spaceX._populate(['ceo', 'president']);
* console.log(`Result: `, result);
* ```
* ```
* Result: {
* name: 'Space X',
* president: {
* name: 'John Smith',
* age: 52,
* address: 'bc7ea8a8-8d1c-4ab6-990c-d3a0163f7e10',
* id: '50e85ac9-5b4f-4587-aeb6-b287527794c9',
* _type: 'Person'
* },
* ceo: {
* name: 'Jane Doe',
* age: 45,
* address: '235dd441-b445-4b88-b6aa-2ce35a958a32',
* id: '32c2e85a-cc91-4db2-935f-f7d2768168de',
* _type: 'Person'
* },
* id: 'd59efbdf-4b7e-4d2f-a986-6e8451f22822',
* _type: 'Company'
* }
* ```
*
* If you want to get only a portion of the object
* ```typescript
* const result = await spaceX._populate({
* ceo: ['age', 'address'], // or 'age,addres'
* president: 'name',
* });
* console.log(`Result: `, result);
* ```
* ```
* Result: {
* name: 'Space X',
* president: { name: 'John Smith' },
* ceo: { age: 45, address: '235dd441-b445-4b88-b6aa-2ce35a958a32' },
* id: 'd59efbdf-4b7e-4d2f-a986-6e8451f22822',
* _type: 'Company'
* }
* ```
*
* Now let's to go deeper
* ```typescript
* const result = await spaceX._populate(
* {
* ceo: {
* select: 'age,id', // remember you can use ['age','address']
* populate: 'address', // will populate all the fields
* },
* president: {
* select: 'name',
* populate: {
* address: 'address', // will populate address field only
* },
* },
* },
* 2, // for populate up to the second level
* );
* console.log(`Result: `, result);
* ```
* ```
* Result: {
* name: 'Space X',
* president: {
* name: 'John Smith',
* address: { address: '13 Washington Square S, New York, NY 10012, USA' }
* },
* ceo: {
* age: 45,
* id: '32c2e85a-cc91-4db2-935f-f7d2768168de',
* address: {
* address: '55 Clark St, Brooklyn, NY 11201, USA',
* id: '235dd441-b445-4b88-b6aa-2ce35a958a32',
* _type: 'Address'
* }
* },
* id: 'd59efbdf-4b7e-4d2f-a986-6e8451f22822',
* _type: 'Company'
* }
*```
* Below is another way through the find functions
* ```typescript
* const result = await Company.findOne(
* { name: 'Space X' },
* {
* select: 'president,ceo',
* populate: {
* president: { select: 'address,id', populate: 'address' },
* ceo: { select: ['age', 'name'], populate: { address: { select: 'id' } } },
* },
* populateMaxDeep: 2,
* },
* );
* console.log(`Result: `, result);
* ```
* ```
* Result: {
* ceo: {
* age: 45,
* name: 'Jane Doe',
* address: { id: '235dd441-b445-4b88-b6aa-2ce35a958a32' }
* },
* president: {
* address: {
* address: '13 Washington Square S, New York, NY 10012, USA',
* id: 'bc7ea8a8-8d1c-4ab6-990c-d3a0163f7e10',
* _type: 'Address'
* },
* id: '50e85ac9-5b4f-4587-aeb6-b287527794c9'
* }
* }
* ```
*/
_populate(fieldsName, options) {
return __awaiter(this, void 0, void 0, function* () {
const { schema, modelName, ottoman } = this.$;
yield (0, model_utils_1.getPopulated)(Object.assign(Object.assign({}, options), { fieldsName, pojo: this, schema, modelName, ottoman }));
return this;
});
}
/**
* Reverts population. Switches back document reference.
*
* @example
* To get in context about the Card and Issue Models [see the populate example.](/docs/api/classes/document.html).
* ```javascript
* const card = await Card.findById(cardId);
* console.log(card.issues); // ['issueId']
*
* await card._populate('issues')
* console.log(card.issues); // [{ id: 'issueId', title: 'Broken card' }]
*
* card._depopulate('issues')
* console.log(card.issues); // ['issueId']
* ```
*/
_depopulate(fieldsName) {
let fieldsToPopulate;
if (fieldsName) {
fieldsToPopulate = (0, schema_utils_1.extractSchemaReferencesFromGivenFields)(fieldsName, this.$.schema);
}
else {
fieldsToPopulate = (0, schema_utils_1.extractSchemaReferencesFields)(this.$.schema);
}
for (const fieldName in fieldsToPopulate) {
const data = this[fieldName];
if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) {
const field = data[i];
if (field && field._getId && field._getId()) {
data[i] = field._getId();
}
}
}
else if (typeof data === 'object') {
if (data && data._getId && data._getId()) {
this[fieldName] = data._getId();
}
}
}
return this;
}
/**
* Allows to know if a document field is populated.
*
* @example
* To get in context about the Card and Issue Models [see the populate example.](/docs/api/classes/document.html).
* ```javascript
* const card = await Card.findById(cardId);
* console.log(card.issues); // ['issueId']
* console.log(card._populated('issues')); // false
*
* await card._populate('issues')
* console.log(card.issues); // [{ id: 'issueId', title: 'Broken card' }]
* console.log(card._populated('issues')); // true
* ```
*/
_populated(fieldName) {
let data = this[fieldName];
data = Array.isArray(data) ? data : [data];
for (const field of data) {
if (!(field && field[this.$.ID_KEY])) {
return false;
}
}
return true;
}
/**
* Allows to easily apply data from an object to current document.
*
* @example
* ```typescript
* const user = new User({ name: "John Doe" });
*
* user._applyData({ name: "Jane Doe" });
* console.log(user) // { name: "Jane Doe" }
* ```
*
* @example With strategies on immutable properties
* ```typescript
* const User = model('Card', { name: { type: String, immutable: true } });
* const user = new User({ name: 'John Doe' });
*
* // with strategy:false is like above example
* user._applyData({ name: 'Jane Doe' }, false);
* console.log(user); // { name: "Jane Doe" }
*
* // with strategy:true remains immutable
* user._applyData({ name: 'Jane Doe' }, true);
* console.log(user); // { name: "John Doe" }
*
* // trying to update it directly
* user.name = 'Jane Doe';
* console.log(user); // { name: "John Doe" }
*
* // with strategy:CAST_STRATEGY.THROW
* user._applyData({ name: 'Jane Doe' }, CAST_STRATEGY.THROW);
* // ImmutableError: Field 'name' is immutable and current cast strategy is set to 'throw'
* ```
*/
_applyData(data, strategy = true) {
var _a, _b;
this.setCurrentStrategy(strategy);
const strict = this.$.schema.options.strict;
for (const key in data) {
this[key] = data[key];
const isImmutable = (_b = (_a = this.$.schema.fields[key]) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.immutable;
if (!this.immutableHasOwnProperty(key) && isImmutable && strict) {
this.setImmutable(key, data[key]);
Object.defineProperty(this, key, {
get() {
return this.getImmutable(key);
},
set(value) {
if (__classPrivateFieldGet(this, _Document_isNew, "f")) {
this.setImmutable(key, value);
return;
}
const currentStrategy = this.getCurrentStrategy();
if (currentStrategy === cast_strategy_1.CAST_STRATEGY.THROW && this.getImmutable(key) !== value) {
throw new ottoman_errors_1.ImmutableError(`Field '${key}' is immutable and current cast strategy is set to 'throw'`);
}
if (typeof currentStrategy === 'boolean' && !currentStrategy) {
this.setImmutable(key, value);
}
},
});
}
}
return this;
}
/**
* Runs schema validations over current document.
* @example
* ```javascript
* const user = new User( { name: "John Doe" } );
*
* try {
* await user._validate()
* } catch(errors) {
* console.log(errors)
* }
* ```
*/
_validate() {
return (0, schema_1.validate)(this, this.$.schema);
}
/**
* Returns a Javascript object with data
*/
toObject() {
return this.$toObject();
}
/**
* Returns a Javascript object to be serialized to JSON
*/
toJSON() {
return this.$toObject();
}
/**
* Encapsulate logic for toObject and toJson
* @ignore
*/
$toObject() {
return Object.assign({}, this);
}
/**
* Boolean flag specifying if the document is new.
* @example
* ```typescript
* const CardSchema = new Schema({
* cardNumber: { type: String, immutable: true },
* zipCode: String,
* });
*
* // Create model
* const Card = model('Card', CardSchema);
*
* // Create document
* const myCard = new Card({ cardNumber: '4321 4321 4321 4321', zipCode: '43210' });
* myCard.$isNew; // true
*
* // Save document
* const myCardSaved = await myCard.save();
* myCardSaved.$isNew; // false
* ```
*/
get $isNew() {
return __classPrivateFieldGet(this, _Document_isNew, "f");
}
}
exports.Document = Document;
_Document_isNew = new WeakMap();
//# sourceMappingURL=document.js.map