UNPKG

ottoman

Version:
627 lines 22.7 kB
"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