You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

193 lines
8.0 KiB
JavaScript

/*
* 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 <caption>Creating a Mapper instance with some options for the model 'User'</caption>
* const mappingOptions = {
* models: {
* 'User': {
* tables: ['users'],
* mappings: new UnderscoreCqlToCamelCaseMappings(),
* columnNames: {
* 'userid': 'id'
* }
* }
* }
* };
* const mapper = new Mapper(client, mappingOptions);
* @example <caption>Creating a Mapper instance with other possible options for a model</caption>
* 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<ModelBatchItem>} 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.
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
* @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<Result>} 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<String, ModelOptions>} 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<String>|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.<String, String>} [columnNames] An associative array containing the name of the columns and
* properties that doesn't follow the convention defined in the <code>TableMappings</code>.
* @property {String} [keyspace] The name of the keyspace. Only mandatory when the Client is not using a keyspace.
*/
module.exports = Mapper;