/* * Copyright DataStax, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; const ModelMapper = require('./model-mapper'); const MappingHandler = require('./mapping-handler'); const DocInfoAdapter = require('./doc-info-adapter'); const errors = require('../errors'); const Result = require('./result'); const ResultMapper = require('./result-mapper'); const ModelMappingInfo = require('./model-mapping-info'); const { ModelBatchItem } = require('./model-batch-item'); /** * Represents an object mapper for Apache Cassandra and DataStax Enterprise. * @alias module:mapping~Mapper * @example Creating a Mapper instance with some options for the model 'User' * const mappingOptions = { * models: { * 'User': { * tables: ['users'], * mappings: new UnderscoreCqlToCamelCaseMappings(), * columnNames: { * 'userid': 'id' * } * } * } * }; * const mapper = new Mapper(client, mappingOptions); * @example Creating a Mapper instance with other possible options for a model * const mappingOptions = { * models: { * 'Video': { * tables: ['videos', 'user_videos', 'latest_videos', { name: 'my_videos_view', isView: true }], * mappings: new UnderscoreCqlToCamelCaseMappings(), * columnNames: { * 'videoid': 'id' * }, * keyspace: 'ks1' * } * } * }; * const mapper = new Mapper(client, mappingOptions); */ class Mapper { /** * Creates a new instance of Mapper. * @param {Client} client The Client instance to use to execute the queries and fetch the metadata. * @param {MappingOptions} [options] The [MappingOptions]{@link module:mapping~MappingOptions} containing the * information of the models and table mappings. */ constructor(client, options) { if (!client) { throw new Error('client must be defined'); } /** * The Client instance used to create this Mapper instance. * @type {Client} */ this.client = client; this._modelMappingInfos = ModelMappingInfo.parse(options, client.keyspace); this._modelMappers = new Map(); } /** * Gets a [ModelMapper]{@link module:mapping~ModelMapper} that is able to map documents of a certain model into * CQL rows. * @param {String} name The name to identify the model. Note that the name is case-sensitive. * @returns {ModelMapper} A [ModelMapper]{@link module:mapping~ModelMapper} instance. */ forModel(name) { let modelMapper = this._modelMappers.get(name); if (modelMapper === undefined) { let mappingInfo = this._modelMappingInfos.get(name); if (mappingInfo === undefined) { if (!this.client.keyspace) { throw new Error(`No mapping information found for model '${name}'. ` + `Mapper is unable to create default mappings without setting the keyspace`); } mappingInfo = ModelMappingInfo.createDefault(name, this.client.keyspace); this.client.log('info', `Mapping information for model '${name}' not found, creating default mapping. ` + `Keyspace: ${mappingInfo.keyspace}; Table: ${mappingInfo.tables[0].name}.`); } else { this.client.log('info', `Creating model mapper for '${name}' using mapping information. Keyspace: ${ mappingInfo.keyspace}; Table${mappingInfo.tables.length > 1? 's' : ''}: ${ mappingInfo.tables.map(t => t.name)}.`); } modelMapper = new ModelMapper(name, new MappingHandler(this.client, mappingInfo)); this._modelMappers.set(name, modelMapper); } return modelMapper; } /** * Executes a batch of queries represented in the items. * @param {Array} items * @param {Object|String} [executionOptions] An object containing the options to be used for the requests * execution or a string representing the name of the execution profile. * @param {String} [executionOptions.executionProfile] The name of the execution profile. * @param {Boolean} [executionOptions.isIdempotent] Defines whether the query can be applied multiple times without * changing the result beyond the initial application. *

* The mapper uses the generated queries to determine the default value. When an UPDATE is generated with a * counter column or appending/prepending to a list column, the execution is marked as not idempotent. *

*

* Additionally, the mapper uses the safest approach for queries with lightweight transactions (Compare and * Set) by considering them as non-idempotent. Lightweight transactions at client level with transparent retries can * break linearizability. If that is not an issue for your application, you can manually set this field to true. *

* @param {Boolean} [executionOptions.logged=true] Determines whether the batch should be written to the batchlog. * @param {Number|Long} [executionOptions.timestamp] The default timestamp for the query in microseconds from the * unix epoch (00:00:00, January 1st, 1970). * @returns {Promise} A Promise that resolves to a [Result]{@link module:mapping~Result}. */ batch(items, executionOptions) { if (!Array.isArray(items) || !(items.length > 0)) { return Promise.reject( new errors.ArgumentError('First parameter items should be an Array with 1 or more ModelBatchItem instances')); } const queries = []; let isIdempotent = true; let isCounter; return Promise .all(items .map(item => { if (!(item instanceof ModelBatchItem)) { return Promise.reject(new Error( 'Batch items must be instances of ModelBatchItem, use modelMapper.batching object to create each item')); } return item.pushQueries(queries) .then(options => { // The batch is idempotent when all the queries contained are idempotent isIdempotent = isIdempotent && options.isIdempotent; // Let it fail at server level when there is a mix of counter and normal mutations isCounter = options.isCounter; }); })) .then(() => this.client.batch(queries, DocInfoAdapter.adaptBatchOptions(executionOptions, isIdempotent, isCounter))) .then(rs => { // Results should only be adapted when the batch contains LWT (single table) const info = items[0].getMappingInfo(); return new Result(rs, info, ResultMapper.getMutationAdapter(rs)); }); } } /** * Represents the mapping options. * @typedef {Object} module:mapping~MappingOptions * @property {Object} models An associative array containing the * name of the model as key and the table and column information as value. */ /** * Represents a set of options that applies to a certain model. * @typedef {Object} module:mapping~ModelOptions * @property {Array|Array<{name, isView}>} tables An Array containing the name of the tables or An Array * containing the name and isView property to describe the table. * @property {TableMappings} mappings The TableMappings implementation instance that is used to convert from column * names to property names and the other way around. * @property {Object.} [columnNames] An associative array containing the name of the columns and * properties that doesn't follow the convention defined in the TableMappings. * @property {String} [keyspace] The name of the keyspace. Only mandatory when the Client is not using a keyspace. */ module.exports = Mapper;