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
193 lines
8.0 KiB
JavaScript
2 years ago
|
/*
|
||
|
* 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;
|