"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.
*
* 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<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 `TableMappings`.
* @property {String} [keyspace] The name of the keyspace. Only mandatory when the Client is not using a keyspace.
*/
module.exports = Mapper;