Node JS version

This commit is contained in:
Aravind142857
2023-06-09 20:08:47 -05:00
parent 8983f0dd80
commit a8b8883b11
894 changed files with 152408 additions and 73 deletions

207
node_modules/cassandra-driver/lib/mapping/cache.js generated vendored Normal file
View File

@@ -0,0 +1,207 @@
/*
* 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 qModule = require('./q');
const QueryOperator = qModule.QueryOperator;
const QueryAssignment = qModule.QueryAssignment;
/**
* Provides utility methods for obtaining a caching keys based on the specifics of the Mapper methods.
* @ignore
*/
class Cache {
/**
* Gets an iterator of keys to uniquely identify a document shape for a select query.
* @param {Array<String>} docKeys
* @param {Object} doc
* @param {{fields, limit, orderBy}} docInfo
* @returns {Iterator}
*/
static *getSelectKey(docKeys, doc, docInfo) {
yield* Cache._yieldKeyAndOperators(docKeys, doc);
yield* Cache._getSelectDocInfo(docInfo);
}
/**
* Gets an iterator of keys to uniquely identify a shape for a select all query.
* @param {{fields, limit, orderBy}} docInfo
* @returns {Iterator}
*/
static *getSelectAllKey(docInfo) {
yield 'root';
yield* Cache._getSelectDocInfo(docInfo);
}
/**
* Gets the parts of the key for a select query related to the docInfo.
* @param {{fields, limit, orderBy}} docInfo
* @private
*/
static *_getSelectDocInfo(docInfo) {
if (docInfo) {
if (docInfo.fields && docInfo.fields.length > 0) {
// Use a separator from properties
yield '|f|';
yield* docInfo.fields;
}
if (typeof docInfo.limit === 'number') {
yield '|l|';
}
if (docInfo.orderBy) {
yield '|o|';
// orderBy is uses property names as keys and 'asc'/'desc' as values
const keys = Object.keys(docInfo.orderBy);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
yield key;
yield docInfo.orderBy[key];
}
}
}
}
/**
* Gets an iterator of keys to uniquely identify a document shape for an insert query.
* @param {Array<String>} docKeys
* @param {{ifNotExists, ttl, fields}} docInfo
* @returns {Iterator}
*/
static *getInsertKey(docKeys, docInfo) {
// No operator supported on INSERT values
yield* docKeys;
if (docInfo) {
if (docInfo.fields && docInfo.fields.length > 0) {
// Use a separator from properties
yield '|f|';
yield* docInfo.fields;
}
if (typeof docInfo.ttl === 'number') {
yield '|t|';
}
if (docInfo.ifNotExists) {
yield '|e|';
}
}
}
/**
* Gets an iterator of keys to uniquely identify a document shape for an UPDATE query.
* @param {Array<String>} docKeys
* @param {Object} doc
* @param {{ifExists, when, ttl, fields}} docInfo
*/
static *getUpdateKey(docKeys, doc, docInfo) {
yield* Cache._yieldKeyAndAllQs(docKeys, doc);
if (docInfo) {
if (docInfo.fields && docInfo.fields.length > 0) {
// Use a separator from properties
yield '|f|';
yield* docInfo.fields;
}
if (typeof docInfo.ttl === 'number') {
yield '|t|';
}
if (docInfo.ifExists) {
yield '|e|';
}
if (docInfo.when) {
yield* Cache._yieldKeyAndOperators(Object.keys(docInfo.when), docInfo.when);
}
}
}
/**
* Gets an iterator of keys to uniquely identify a document shape for a DELETE query.
* @param {Array<String>} docKeys
* @param {Object} doc
* @param {{ifExists, when, fields, deleteOnlyColumns}} docInfo
* @returns {Iterator}
*/
static *getRemoveKey(docKeys, doc, docInfo) {
yield* Cache._yieldKeyAndOperators(docKeys, doc);
if (docInfo) {
if (docInfo.fields && docInfo.fields.length > 0) {
// Use a separator from properties
yield '|f|';
yield* docInfo.fields;
}
if (docInfo.ifExists) {
yield '|e|';
}
if (docInfo.deleteOnlyColumns) {
yield '|dc|';
}
if (docInfo.when) {
yield* Cache._yieldKeyAndOperators(Object.keys(docInfo.when), docInfo.when);
}
}
}
static *_yieldKeyAndOperators(keys, obj) {
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
yield key;
yield* Cache._yieldOperators(obj[key]);
}
}
static *_yieldOperators(value) {
if (value !== null && value !== undefined && value instanceof QueryOperator) {
yield value.key;
if (value.hasChildValues) {
yield* Cache._yieldOperators(value.value[0]);
yield '|/|';
yield* Cache._yieldOperators(value.value[1]);
}
}
}
static *_yieldKeyAndAllQs(keys, obj) {
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
yield key;
const value = obj[key];
if (value !== null && value !== undefined) {
if (value instanceof QueryOperator) {
yield* Cache._yieldOperators(value);
}
else if (value instanceof QueryAssignment) {
yield value.sign;
yield value.inverted;
}
}
}
}
}
module.exports = Cache;

View File

@@ -0,0 +1,162 @@
/*
* 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 errors = require('../errors');
const utils = require('../utils');
/**
* Provides utility methods to adapt and map user provided docInfo and executionOptions to a predictable object format.
* @ignore
*/
class DocInfoAdapter {
/**
* Returns an Array where each item contains the property name, the column name and the property value (to obtain
* the operator).
* When docInfo.fields is specified, it uses that array to obtain the information.
* @param {Array<String>} docKeys
* @param {null|{fields}} docInfo
* @param {Object} doc
* @param {ModelMappingInfo} mappingInfo
* @returns {Array}
*/
static getPropertiesInfo(docKeys, docInfo, doc, mappingInfo) {
let propertyKeys = docKeys;
if (docInfo && docInfo.fields && docInfo.fields.length > 0) {
propertyKeys = docInfo.fields;
}
return propertyKeys.map(propertyName => ({
propertyName,
columnName: mappingInfo.getColumnName(propertyName),
value: doc[propertyName],
fromModel: mappingInfo.getFromModelFn(propertyName)
}));
}
/**
* @param {{orderBy}} docInfo
* @param {ModelMappingInfo} mappingInfo
* @returns {Array<String>}
*/
static adaptOrderBy(docInfo, mappingInfo){
if (!docInfo || !docInfo.orderBy) {
return utils.emptyArray;
}
return Object.keys(docInfo.orderBy).map(key => {
const value = docInfo.orderBy[key];
const ordering = typeof value === 'string' ? value.toUpperCase() : value;
if (ordering !== 'ASC' && ordering !== 'DESC') {
throw new errors.ArgumentError('Order must be either "ASC" or "DESC", obtained: ' + value);
}
return [ mappingInfo.getColumnName(key), ordering ];
});
}
/**
* Returns the QueryOptions for an INSERT/UPDATE/DELETE statement.
* @param {Object|String|undefined} executionOptions
* @param {Boolean} isIdempotent
*/
static adaptOptions(executionOptions, isIdempotent) {
const options = {
prepare: true,
executionProfile: undefined,
timestamp: undefined,
isIdempotent: isIdempotent
};
if (typeof executionOptions === 'string') {
options.executionProfile = executionOptions;
}
else if (executionOptions !== null && executionOptions !== undefined) {
options.executionProfile = executionOptions.executionProfile;
options.timestamp = executionOptions.timestamp;
if (executionOptions.isIdempotent !== undefined) {
options.isIdempotent = executionOptions.isIdempotent;
}
}
return options;
}
/**
* Returns the QueryOptions for a SELECT statement.
* @param {Object|String|undefined} executionOptions
* @param {Boolean} [overrideIdempotency]
*/
static adaptAllOptions(executionOptions, overrideIdempotency) {
const options = {
prepare: true,
executionProfile: undefined,
fetchSize: undefined,
pageState: undefined,
timestamp: undefined,
isIdempotent: undefined
};
if (typeof executionOptions === 'string') {
options.executionProfile = executionOptions;
}
else if (executionOptions !== null && executionOptions !== undefined) {
options.executionProfile = executionOptions.executionProfile;
options.fetchSize = executionOptions.fetchSize;
options.pageState = executionOptions.pageState;
options.timestamp = executionOptions.timestamp;
options.isIdempotent = executionOptions.isIdempotent;
}
if (overrideIdempotency) {
options.isIdempotent = true;
}
return options;
}
/**
* Returns the QueryOptions for a batch statement.
* @param {Object|String|undefined} executionOptions
* @param {Boolean} isIdempotent
* @param {Boolean} isCounter
*/
static adaptBatchOptions(executionOptions, isIdempotent, isCounter) {
const options = {
prepare: true,
executionProfile: undefined,
timestamp: undefined,
logged: undefined,
isIdempotent: isIdempotent,
counter: isCounter
};
if (typeof executionOptions === 'string') {
options.executionProfile = executionOptions;
}
else if (executionOptions !== null && executionOptions !== undefined) {
options.executionProfile = executionOptions.executionProfile;
options.timestamp = executionOptions.timestamp;
options.logged = executionOptions.logged !== false;
if (executionOptions.isIdempotent !== undefined) {
options.isIdempotent = executionOptions.isIdempotent;
}
}
return options;
}
}
module.exports = DocInfoAdapter;

189
node_modules/cassandra-driver/lib/mapping/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,189 @@
/*
* 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.
*/
import { types } from '../types';
import { Client } from '../../';
import Long = types.Long;
export namespace mapping {
interface TableMappings {
getColumnName(propName: string): string;
getPropertyName(columnName: string): string;
newObjectInstance(): any;
}
class DefaultTableMappings implements TableMappings {
getColumnName(propName: string): string;
getPropertyName(columnName: string): string;
newObjectInstance(): any;
}
class UnderscoreCqlToCamelCaseMappings implements TableMappings {
getColumnName(propName: string): string;
getPropertyName(columnName: string): string;
newObjectInstance(): any;
}
interface Result<T = any> extends Iterator<T> {
wasApplied(): boolean;
first(): T | null;
forEach(callback: (currentValue: T, index: number) => void, thisArg?: any): void;
toArray(): T[];
}
type MappingExecutionOptions = {
executionProfile?: string;
isIdempotent?: boolean;
logged?: boolean;
timestamp?: number | Long;
fetchSize?: number;
pageState?: number;
}
interface ModelTables {
name: string;
isView: boolean;
}
class Mapper {
constructor(client: Client, options?: MappingOptions);
batch(items: ModelBatchItem[], executionOptions?: string | MappingExecutionOptions): Promise<Result>;
forModel<T = any>(name: string): ModelMapper<T>;
}
type MappingOptions = {
models: { [key: string]: ModelOptions };
}
type FindDocInfo = {
fields?: string[];
orderBy?: { [key: string]: string };
limit?: number;
}
type InsertDocInfo = {
fields?: string[];
ttl?: number;
ifNotExists?: boolean;
}
type UpdateDocInfo = {
fields?: string[];
ttl?: number;
ifExists?: boolean;
when?: { [key: string]: any };
orderBy?: { [key: string]: string };
limit?: number;
deleteOnlyColumns?: boolean;
}
type RemoveDocInfo = {
fields?: string[];
ttl?: number;
ifExists?: boolean;
when?: { [key: string]: any };
deleteOnlyColumns?: boolean;
}
type ModelOptions = {
tables?: string[] | ModelTables[];
mappings?: TableMappings;
columns?: { [key: string]: string|ModelColumnOptions };
keyspace?: string;
}
type ModelColumnOptions = {
name: string;
toModel?: (columnValue: any) => any;
fromModel?: (modelValue: any) => any;
};
interface ModelBatchItem {
}
interface ModelBatchMapper {
insert(doc: any, docInfo?: InsertDocInfo): ModelBatchItem;
remove(doc: any, docInfo?: RemoveDocInfo): ModelBatchItem;
update(doc: any, docInfo?: UpdateDocInfo): ModelBatchItem;
}
interface ModelMapper<T = any> {
name: string;
batching: ModelBatchMapper;
get(doc: { [key: string]: any }, docInfo?: { fields?: string[] }, executionOptions?: string | MappingExecutionOptions): Promise<null | T>;
find(doc: { [key: string]: any }, docInfo?: FindDocInfo, executionOptions?: string | MappingExecutionOptions): Promise<Result<T>>;
findAll(docInfo?: FindDocInfo, executionOptions?: string | MappingExecutionOptions): Promise<Result<T>>;
insert(doc: { [key: string]: any }, docInfo?: InsertDocInfo, executionOptions?: string | MappingExecutionOptions): Promise<Result<T>>;
update(doc: { [key: string]: any }, docInfo?: UpdateDocInfo, executionOptions?: string | MappingExecutionOptions): Promise<Result<T>>;
remove(doc: { [key: string]: any }, docInfo?: RemoveDocInfo, executionOptions?: string | MappingExecutionOptions): Promise<Result<T>>;
mapWithQuery(
query: string,
paramsHandler: (doc: any) => any[],
executionOptions?: string | MappingExecutionOptions
): (doc: any, executionOptions?: string | MappingExecutionOptions) => Promise<Result<T>>;
}
namespace q {
interface QueryOperator {
}
function in_(arr: any): QueryOperator;
function gt(value: any): QueryOperator;
function gte(value: any): QueryOperator;
function lt(value: any): QueryOperator;
function lte(value: any): QueryOperator;
function notEq(value: any): QueryOperator;
function and(condition1: any, condition2: any): QueryOperator;
function incr(value: any): QueryOperator;
function decr(value: any): QueryOperator;
function append(value: any): QueryOperator;
function prepend(value: any): QueryOperator;
function remove(value: any): QueryOperator;
}
}

33
node_modules/cassandra-driver/lib/mapping/index.js generated vendored Normal file
View File

@@ -0,0 +1,33 @@
/*
* 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';
/**
* Module containing classes and fields related to the Mapper.
* @module mapping
*/
exports.Mapper = require('./mapper');
exports.ModelMapper = require('./model-mapper');
exports.ModelBatchMapper = require('./model-batch-mapper');
exports.ModelBatchItem = require('./model-batch-item').ModelBatchItem;
exports.Result = require('./result');
const tableMappingsModule = require('./table-mappings');
exports.TableMappings = tableMappingsModule.TableMappings;
exports.DefaultTableMappings = tableMappingsModule.DefaultTableMappings;
exports.UnderscoreCqlToCamelCaseMappings = tableMappingsModule.UnderscoreCqlToCamelCaseMappings;
exports.q = require('./q').q;

193
node_modules/cassandra-driver/lib/mapping/mapper.js generated vendored Normal file
View File

@@ -0,0 +1,193 @@
/*
* 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;

View File

@@ -0,0 +1,412 @@
/*
* 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 utils = require('../utils');
const QueryGenerator = require('./query-generator');
const ResultMapper = require('./result-mapper');
const Result = require('./result');
const Cache = require('./cache');
const Tree = require('./tree');
const ObjectSelector = require('./object-selector');
const DocInfoAdapter = require('./doc-info-adapter');
const cacheHighWaterMark = 100;
/**
* @ignore
*/
class MappingHandler {
/**
* @param {Client} client
* @param {ModelMappingInfo} mappingInfo
*/
constructor(client, mappingInfo) {
this._client = client;
this._cache = {
select: new Tree().on('add', length => this._validateCacheLength(length)),
selectAll: new Tree().on('add', length => this._validateCacheLength(length)),
insert: new Tree().on('add', length => this._validateCacheLength(length)),
update: new Tree().on('add', length => this._validateCacheLength(length)),
remove: new Tree().on('add', length => this._validateCacheLength(length)),
customQueries: new Map()
};
/**
* Gets the mapping information of the document.
* @type {ModelMappingInfo}
*/
this.info = mappingInfo;
}
/**
* Gets a function to be used to execute SELECT the query using the document.
* @param {Object} doc
* @param {{fields, orderBy, limit}} docInfo
* @param {Boolean} allPKsDefined Determines whether all primary keys must be defined in the doc for the query to
* be valid.
* @return {Promise<Function>}
*/
getSelectExecutor(doc, docInfo, allPKsDefined) {
const docKeys = Object.keys(doc);
if (docKeys.length === 0) {
return Promise.reject(new Error('Expected object with keys'));
}
const cacheKey = Cache.getSelectKey(docKeys, doc, docInfo);
// Cache the executor and the result mapper under the same key
// That way, those can get evicted together
const cacheItem = this._cache.select.getOrCreate(cacheKey, () => ({ executor: null, resultAdapter: null }));
if (cacheItem.executor !== null) {
return Promise.resolve(cacheItem.executor);
}
const propertiesInfo = DocInfoAdapter.getPropertiesInfo(docKeys, null, doc, this.info);
const fieldsInfo = DocInfoAdapter.getPropertiesInfo(utils.emptyArray, docInfo, doc, this.info);
const orderByColumns = DocInfoAdapter.adaptOrderBy(docInfo, this.info);
const limit = docInfo && docInfo.limit;
return this._client.connect()
.then(() =>
ObjectSelector.getForSelect(this._client, this.info, allPKsDefined, propertiesInfo, fieldsInfo, orderByColumns))
.then(tableName => {
// Part of the closure
const query = QueryGenerator.getSelect(tableName, this.info.keyspace, propertiesInfo, fieldsInfo,
orderByColumns, limit);
const paramsGetter = QueryGenerator.selectParamsGetter(propertiesInfo, limit);
const self = this;
cacheItem.executor = function selectExecutor(doc, docInfo, executionOptions) {
return self._executeSelect(query, paramsGetter, doc, docInfo, executionOptions, cacheItem);
};
return cacheItem.executor;
});
}
getSelectAllExecutor(docInfo) {
const cacheKey = Cache.getSelectAllKey(docInfo);
const cacheItem = this._cache.selectAll.getOrCreate(cacheKey, () => ({ executor: null, resultAdapter: null }));
if (cacheItem.executor !== null) {
return cacheItem.executor;
}
const fieldsInfo = DocInfoAdapter.getPropertiesInfo(utils.emptyArray, docInfo, utils.emptyObject, this.info);
const orderByColumns = DocInfoAdapter.adaptOrderBy(docInfo, this.info);
const limit = docInfo && docInfo.limit;
const tableName = ObjectSelector.getForSelectAll(this.info);
// Part of the closure
const query = QueryGenerator.getSelect(
tableName, this.info.keyspace, utils.emptyArray, fieldsInfo, orderByColumns, limit);
const paramsGetter = QueryGenerator.selectParamsGetter(utils.emptyArray, limit);
const self = this;
cacheItem.executor = function selectAllExecutor(docInfo, executionOptions) {
return self._executeSelect(query, paramsGetter, null, docInfo, executionOptions, cacheItem);
};
return cacheItem.executor;
}
/**
* Executes a SELECT query and returns the adapted results.
* When a result adapter is not yet created, it gets a new one and caches it.
* @private
*/
_executeSelect(query, paramsGetter, doc, docInfo, executionOptions, cacheItem) {
const options = DocInfoAdapter.adaptAllOptions(executionOptions, true);
return this._client.execute(query, paramsGetter(doc, docInfo, this.info), options)
.then(rs => {
if (cacheItem.resultAdapter === null) {
cacheItem.resultAdapter = ResultMapper.getSelectAdapter(this.info, rs);
}
return new Result(rs, this.info, cacheItem.resultAdapter);
});
}
/**
* Gets a function to be used to execute INSERT the query using the document.
* @param {Object} doc
* @param {{ifNotExists, ttl, fields}} docInfo
* @return {Promise<Function>}
*/
getInsertExecutor(doc, docInfo) {
const docKeys = Object.keys(doc);
if (docKeys.length === 0) {
return Promise.reject(new Error('Expected object with keys'));
}
const cacheKey = Cache.getInsertKey(docKeys, docInfo);
const cacheItem = this._cache.insert.getOrCreate(cacheKey, () => ({ executor: null }));
if (cacheItem.executor !== null) {
return Promise.resolve(cacheItem.executor);
}
return this.createInsertQueries(docKeys, doc, docInfo)
.then(queries => {
if (queries.length === 1) {
return this._setSingleExecutor(cacheItem, queries[0]);
}
return this._setBatchExecutor(cacheItem, queries);
});
}
/**
* Creates an Array containing the query and the params getter function for each table affected by the INSERT.
* @param {Array<String>} docKeys
* @param {Object} doc
* @param {{ifNotExists, ttl, fields}} docInfo
* @returns {Promise<Array<{query, paramsGetter}>>}
*/
createInsertQueries(docKeys, doc, docInfo) {
const propertiesInfo = DocInfoAdapter.getPropertiesInfo(docKeys, docInfo, doc, this.info);
const ifNotExists = docInfo && docInfo.ifNotExists;
// Get all the tables affected
return this._client.connect()
.then(() => ObjectSelector.getForInsert(this._client, this.info, propertiesInfo))
.then(tables => {
if (tables.length > 1 && ifNotExists) {
throw new Error('Batch with ifNotExists conditions cannot span multiple tables');
}
// For each tables affected, Generate query and parameter getters
return tables.map(table =>
QueryGenerator.getInsert(table, this.info.keyspace, propertiesInfo, docInfo,ifNotExists));
});
}
/**
* Gets a function to be used to execute the UPDATE queries with the provided document.
* @param {Object} doc
* @param {{ifExists, when, ttl, fields}} docInfo
* @return {Promise<Function>}
*/
getUpdateExecutor(doc, docInfo) {
const docKeys = Object.keys(doc);
if (docKeys.length === 0) {
return Promise.reject(new Error('Expected object with keys'));
}
const cacheKey = Cache.getUpdateKey(docKeys, doc, docInfo);
const cacheItem = this._cache.update.getOrCreate(cacheKey, () => ({ executor: null }));
if (cacheItem.executor !== null) {
return Promise.resolve(cacheItem.executor);
}
return this.createUpdateQueries(docKeys, doc, docInfo)
.then(queries => {
if (queries.length === 1) {
return this._setSingleExecutor(cacheItem, queries[0]);
}
return this._setBatchExecutor(cacheItem, queries);
});
}
/**
* Creates an Array containing the query and the params getter function for each table affected by the UPDATE.
* @param {Array<String>} docKeys
* @param {Object} doc
* @param {Object} docInfo
* @returns {Promise<Array<{query, paramsGetter, isIdempotent}>>}
*/
createUpdateQueries(docKeys, doc, docInfo) {
const propertiesInfo = DocInfoAdapter.getPropertiesInfo(docKeys, docInfo, doc, this.info);
const ifExists = docInfo && docInfo.ifExists;
const when = docInfo && docInfo.when
? DocInfoAdapter.getPropertiesInfo(Object.keys(docInfo.when), null, docInfo.when, this.info)
: utils.emptyArray;
if (when.length > 0 && ifExists) {
throw new Error('Both when and ifExists conditions can not be applied to the same statement');
}
// Get all the tables affected
return this._client.connect()
.then(() => ObjectSelector.getForUpdate(this._client, this.info, propertiesInfo, when))
.then(tables => {
if (tables.length > 1 && (when.length > 0 || ifExists)) {
throw new Error('Batch with when or ifExists conditions cannot span multiple tables');
}
// For each table affected, Generate query and parameter getters
return tables.map(table =>
QueryGenerator.getUpdate(table, this.info.keyspace, propertiesInfo, docInfo, when, ifExists));
});
}
/**
* Gets a function to be used to execute the DELETE queries with the provided document.
* @param {Object} doc
* @param {{when, ifExists, fields, deleteOnlyColumns}} docInfo
* @return {Promise<Function>}
*/
getDeleteExecutor(doc, docInfo) {
const docKeys = Object.keys(doc);
if (docKeys.length === 0) {
return Promise.reject(new Error('Expected object with keys'));
}
const cacheKey = Cache.getRemoveKey(docKeys, doc, docInfo);
const cacheItem = this._cache.remove.getOrCreate(cacheKey, () => ({ executor: null }));
if (cacheItem.executor !== null) {
return Promise.resolve(cacheItem.executor);
}
return this.createDeleteQueries(docKeys, doc, docInfo)
.then(queries => {
if (queries.length === 1) {
return this._setSingleExecutor(cacheItem, queries[0]);
}
return this._setBatchExecutor(cacheItem, queries);
});
}
/**
* Creates an Array containing the query and the params getter function for each table affected by the DELETE.
* @param {Array<String>} docKeys
* @param {Object} doc
* @param {{when, ifExists, fields, deleteOnlyColumns}} docInfo
* @returns {Promise<Array<{query, paramsGetter}>>}
*/
createDeleteQueries(docKeys, doc, docInfo) {
const propertiesInfo = DocInfoAdapter.getPropertiesInfo(docKeys, docInfo, doc, this.info);
const ifExists = docInfo && docInfo.ifExists;
const when = docInfo && docInfo.when
? DocInfoAdapter.getPropertiesInfo(Object.keys(docInfo.when), null, docInfo.when, this.info)
: utils.emptyArray;
if (when.length > 0 && ifExists) {
throw new Error('Both when and ifExists conditions can not be applied to the same statement');
}
// Get all the tables affected
return this._client.connect()
.then(() => ObjectSelector.getForDelete(this._client, this.info, propertiesInfo, when))
.then(tables => {
if (tables.length > 1 && (when.length > 0 || ifExists)) {
throw new Error('Batch with when or ifExists conditions cannot span multiple tables');
}
// For each tables affected, Generate query and parameter getters
return tables.map(table =>
QueryGenerator.getDelete(table, this.info.keyspace, propertiesInfo, docInfo, when, ifExists));
});
}
getExecutorFromQuery(query, paramsHandler, commonExecutionOptions) {
// Use the current instance in the closure
// as there is no guarantee of how the returned function will be invoked
const self = this;
const commonOptions = commonExecutionOptions ? DocInfoAdapter.adaptAllOptions(commonExecutionOptions) : null;
return (function queryMappedExecutor(doc, executionOptions) {
// When the executionOptions were already specified,
// use it and skip the ones provided in each invocation
const options = commonOptions
? commonOptions
: DocInfoAdapter.adaptAllOptions(executionOptions);
return self._client.execute(query, paramsHandler(doc), options).then(rs => {
// Cache the resultAdapter based on the query
let resultAdapter = self._cache.customQueries.get(query);
if (resultAdapter === undefined) {
const resultAdapterInfo = ResultMapper.getCustomQueryAdapter(self.info, rs);
resultAdapter = resultAdapterInfo.fn;
if (resultAdapterInfo.canCache) {
// Avoid caching conditional updates results as the amount of columns change
// depending on the parameter values.
self._cache.customQueries.set(query, resultAdapter);
if (self._cache.customQueries.size === cacheHighWaterMark) {
self._client.log('warning',
`Custom queries cache reached ${cacheHighWaterMark} items, this could be caused by ` +
`hard-coding parameter values inside the query, which should be avoided`);
}
}
}
return new Result(rs, self.info, resultAdapter);
});
});
}
_setSingleExecutor(cacheItem, queryInfo) {
// Parameters and this instance are part of the closure
const self = this;
// Set the function to execute the request in the cache
cacheItem.executor = function singleExecutor(doc, docInfo, executionOptions) {
const options = DocInfoAdapter.adaptOptions(executionOptions, queryInfo.isIdempotent);
return self._client.execute(queryInfo.query, queryInfo.paramsGetter(doc, docInfo, self.info), options)
.then(rs => new Result(rs, self.info, ResultMapper.getMutationAdapter(rs)));
};
return cacheItem.executor;
}
_setBatchExecutor(cacheItem, queries) {
// Parameters and the following fields are part of the closure
const self = this;
const isIdempotent = queries.reduce((acc, q) => acc && q.isIdempotent, true);
// Set the function to execute the batch request in the cache
cacheItem.executor = function batchExecutor(doc, docInfo, executionOptions) {
// Use the params getter function to obtain the parameters each time
const queryAndParams = queries.map(q => ({
query: q.query,
params: q.paramsGetter(doc, docInfo, self.info)
}));
const options = DocInfoAdapter.adaptOptions(executionOptions, isIdempotent);
// Execute using a Batch
return self._client.batch(queryAndParams, options)
.then(rs => new Result(rs, self.info, ResultMapper.getMutationAdapter(rs)));
};
return cacheItem.executor;
}
_validateCacheLength(length) {
if (length !== cacheHighWaterMark) {
return;
}
this._client.log('warning', `ModelMapper cache reached ${cacheHighWaterMark} items, this could be caused by ` +
`building the object to map in different ways (with different shapes) each time. Use the same or few object ` +
`structures for a model and represent unset values with undefined or types.unset`);
}
}
module.exports = MappingHandler;

View File

@@ -0,0 +1,191 @@
/*
* 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 Cache = require('./cache');
/**
* Represents a query or a set of queries used to perform a mutation in a batch.
* @alias module:mapping~ModelBatchItem
*/
class ModelBatchItem {
/**
* @param {Object} doc
* @param {Object} docInfo
* @param {MappingHandler} handler
* @param {Tree} cache
*/
constructor(doc, docInfo, handler, cache) {
this.doc = doc;
this.docInfo = docInfo;
this.handler = handler;
this.cache = cache;
}
/**
* @ignore
* @returns <Promise<Array>>
*/
getQueries() {
const docKeys = Object.keys(this.doc);
const cacheItem = this.cache.getOrCreate(this.getCacheKey(docKeys), () => ({ queries: null }));
if (cacheItem.queries === null) {
cacheItem.queries = this.createQueries(docKeys);
}
return cacheItem.queries;
}
/**
* Gets the cache key for this item.
* @abstract
* @param {Array} docKeys
* @returns {Iterator}
*/
getCacheKey(docKeys) {
throw new Error('getCacheKey must be implemented');
}
/**
* Gets the Promise to create the queries.
* @abstract
* @param {Array} docKeys
* @returns {Promise<Array>}
*/
createQueries(docKeys) {
throw new Error('getCacheKey must be implemented');
}
/**
* Pushes the queries and parameters represented by this instance to the provided array.
* @internal
* @ignore
* @param {Array} arr
* @return {Promise<{isIdempotent, isCounter}>}
*/
pushQueries(arr) {
let isIdempotent = true;
let isCounter;
return this.getQueries().then(queries => {
queries.forEach(q => {
// It's idempotent if all the queries contained are idempotent
isIdempotent = isIdempotent && q.isIdempotent;
// Either all queries are counter mutation or we let it fail at server level
isCounter = q.isCounter;
arr.push({ query: q.query, params: q.paramsGetter(this.doc, this.docInfo, this.getMappingInfo()) });
});
return { isIdempotent, isCounter };
});
}
/**
* Gets the mapping information for this batch item.
* @internal
* @ignore
*/
getMappingInfo() {
return this.handler.info;
}
}
/**
* Represents a single or a set of INSERT queries in a batch.
* @ignore
* @internal
*/
class InsertModelBatchItem extends ModelBatchItem {
/**
* @param {Object} doc
* @param {Object} docInfo
* @param {MappingHandler} handler
* @param {Tree} cache
*/
constructor(doc, docInfo, handler, cache) {
super(doc, docInfo, handler, cache);
}
/** @override */
getCacheKey(docKeys) {
return Cache.getInsertKey(docKeys, this.docInfo);
}
/** @override */
createQueries(docKeys) {
return this.handler.createInsertQueries(docKeys, this.doc, this.docInfo);
}
}
/**
* Represents a single or a set of UPDATE queries in a batch.
* @ignore
* @internal
*/
class UpdateModelBatchItem extends ModelBatchItem {
/**
* @param {Object} doc
* @param {Object} docInfo
* @param {MappingHandler} handler
* @param {Tree} cache
*/
constructor(doc, docInfo, handler, cache) {
super(doc, docInfo, handler, cache);
}
/** @override */
getCacheKey(docKeys) {
return Cache.getUpdateKey(docKeys, this.doc, this.docInfo);
}
/** @override */
createQueries(docKeys) {
return this.handler.createUpdateQueries(docKeys, this.doc, this.docInfo);
}
}
/**
* Represents a single or a set of DELETE queries in a batch.
* @ignore
* @internal
*/
class RemoveModelBatchItem extends ModelBatchItem {
/**
* @param {Object} doc
* @param {Object} docInfo
* @param {MappingHandler} handler
* @param {Tree} cache
*/
constructor(doc, docInfo, handler, cache) {
super(doc, docInfo, handler, cache);
}
/** @override */
getCacheKey(docKeys) {
return Cache.getRemoveKey(docKeys, this.doc, this.docInfo);
}
/** @override */
createQueries(docKeys) {
return this.handler.createDeleteQueries(docKeys, this.doc, this.docInfo);
}
}
module.exports = { ModelBatchItem, InsertModelBatchItem, UpdateModelBatchItem, RemoveModelBatchItem };

View File

@@ -0,0 +1,125 @@
/*
* 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 Tree = require('./tree');
const moduleBatchItemModule = require('./model-batch-item');
const InsertModelBatchItem = moduleBatchItemModule.InsertModelBatchItem;
const UpdateModelBatchItem = moduleBatchItemModule.UpdateModelBatchItem;
const RemoveModelBatchItem = moduleBatchItemModule.RemoveModelBatchItem;
/**
* Provides utility methods to group multiple mutations on a single batch.
* @alias module:mapping~ModelBatchMapper
*/
class ModelBatchMapper {
/**
* Creates a new instance of model batch mapper.
* <p>
* An instance of this class is exposed as a singleton in the <code>batching</code> field of the
* [ModelMapper]{@link module:mapping~ModelMapper}. Note that new instances should not be create with this
* constructor.
* </p>
* @param {MappingHandler} handler
* @ignore
*/
constructor(handler) {
this._handler = handler;
this._cache = {
insert: new Tree(),
update: new Tree(),
remove: new Tree()
};
}
/**
* Gets a [ModelBatchItem]{@link module:mapping~ModelBatchItem} containing the queries for the INSERT mutation to be
* used in a batch execution.
* @param {Object} doc An object containing the properties to insert.
* @param {Object} [docInfo] An object containing the additional document information.
* @param {Array<String>} [docInfo.fields] An Array containing the name of the properties that will be used in the
* INSERT cql statements generated. If specified, it must include the columns to insert and the primary keys.
* @param {Number} [docInfo.ttl] Specifies an optional Time To Live (in seconds) for the inserted values.
* @param {Boolean} [docInfo.ifNotExists] When set, it only inserts if the row does not exist prior to the insertion.
* <p>Please note that using IF NOT EXISTS will incur a non negligible performance cost so this should be used
* sparingly.</p>
* @returns {ModelBatchItem} A [ModelBatchItem]{@link module:mapping~ModelBatchItem} instance representing a query
* or a set of queries to be included in a batch.
*/
insert(doc, docInfo) {
return new InsertModelBatchItem(doc, docInfo, this._handler, this._cache.insert);
}
/**
* Gets a [ModelBatchItem]{@link module:mapping~ModelBatchItem} containing the queries for the UPDATE mutation to be
* used in a batch execution.
* @param {Object} doc An object containing the properties to update.
* @param {Object} [docInfo] An object containing the additional document information.
* @param {Array<String>} [docInfo.fields] An Array containing the name of the properties that will be used in the
* UPDATE cql statements generated. If specified, it must include the columns to update and the primary keys.
* @param {Number} [docInfo.ttl] Specifies an optional Time To Live (in seconds) for the inserted values.
* @param {Boolean} [docInfo.ifExists] When set, it only updates if the row already exists on the server.
* <p>
* Please note that using IF conditions will incur a non negligible performance cost on the server-side so this
* should be used sparingly.
* </p>
* @param {Object} [docInfo.when] A document that act as the condition that has to be met for the UPDATE to occur.
* Use this property only in the case you want to specify a conditional clause for lightweight transactions (CAS).
* <p>
* Please note that using IF conditions will incur a non negligible performance cost on the server-side so this
* should be used sparingly.
* </p>
* @returns {ModelBatchItem} A [ModelBatchItem]{@link module:mapping~ModelBatchItem} instance representing a query
* or a set of queries to be included in a batch.
*/
update(doc, docInfo) {
return new UpdateModelBatchItem(doc, docInfo, this._handler, this._cache.update);
}
/**
* Gets a [ModelBatchItem]{@link module:mapping~ModelBatchItem} containing the queries for the DELETE mutation to be
* used in a batch execution.
* @param {Object} doc A document containing the primary keys values of the document to delete.
* @param {Object} [docInfo] An object containing the additional doc information.
* @param {Object} [docInfo.when] A document that act as the condition that has to be met for the DELETE to occur.
* Use this property only in the case you want to specify a conditional clause for lightweight transactions (CAS).
* When the CQL query is generated, this would be used to generate the `IF` clause.
* <p>
* Please note that using IF conditions will incur a non negligible performance cost on the server-side so this
* should be used sparingly.
* </p>
* @param {Boolean} [docInfo.ifExists] When set, it only issues the DELETE command if the row already exists on the
* server.
* <p>
* Please note that using IF conditions will incur a non negligible performance cost on the server-side so this
* should be used sparingly.
* </p>
* @param {Array<String>} [docInfo.fields] An Array containing the name of the properties that will be used in the
* DELETE cql statement generated. If specified, it must include the columns to delete and the primary keys.
* @param {Boolean} [docInfo.deleteOnlyColumns] Determines that, when more document properties are specified
* besides the primary keys, the generated DELETE statement should be used to delete some column values but leave
* the row. When this is enabled and more properties are specified, a DELETE statement will have the following form:
* "DELETE col1, col2 FROM table1 WHERE pk1 = ? AND pk2 = ?"
* @returns {ModelBatchItem} A [ModelBatchItem]{@link module:mapping~ModelBatchItem} instance representing a query
* or a set of queries to be included in a batch.
*/
remove(doc, docInfo) {
return new RemoveModelBatchItem(doc, docInfo, this._handler, this._cache.update);
}
}
module.exports = ModelBatchMapper;

View File

@@ -0,0 +1,306 @@
/*
* 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 ModelBatchMapper = require('./model-batch-mapper');
/**
* Represents an object mapper for a specific model.
* @alias module:mapping~ModelMapper
*/
class ModelMapper {
constructor(name, handler) {
/**
* Gets the name identifier of the model.
* @type {String}
*/
this.name = name;
this._handler = handler;
/**
* Gets a [ModelBatchMapper]{@link module:mapping~ModelBatchMapper} instance containing utility methods to group
* multiple doc mutations in a single batch.
* @type {ModelBatchMapper}
*/
this.batching = new ModelBatchMapper(this._handler);
}
/**
* Gets the first document matching the provided filter or null when not found.
* <p>
* Note that all partition and clustering keys must be defined in order to use this method.
* </p>
* @param {Object} doc The object containing the properties that map to the primary keys.
* @param {Object} [docInfo] An object containing the additional document information.
* @param {Array<String>} [docInfo.fields] An Array containing the name of the properties that will be used in the
* SELECT cql statement generated, in order to restrict the amount of columns retrieved.
* @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.
* @return {Promise<Object>}
* @example <caption>Get a video by id</caption>
* videoMapper.get({ id })
* @example <caption>Get a video by id, selecting specific columns</caption>
* videoMapper.get({ id }, fields: ['name', 'description'])
*/
get(doc, docInfo, executionOptions) {
if (executionOptions === undefined && typeof docInfo === 'string') {
executionOptions = docInfo;
docInfo = null;
}
return this._handler.getSelectExecutor(doc, docInfo, true)
.then(executor => executor(doc, docInfo, executionOptions))
.then(result => result.first());
}
/**
* Executes a SELECT query based on the filter and returns the result as an iterable of documents.
* @param {Object} doc An object containing the properties that map to the primary keys to filter.
* @param {Object} [docInfo] An object containing the additional document information.
* @param {Array<String>} [docInfo.fields] An Array containing the name of the properties that will be used in the
* SELECT cql statement generated, in order to restrict the amount of columns retrieved.
* @param {Object<String, String>} [docInfo.orderBy] An associative array containing the column names as key and
* the order string (asc or desc) as value used to set the order of the results server-side.
* @param {Number} [docInfo.limit] Restricts the result of the query to a maximum number of rows on the
* server.
* @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 {Number} [executionOptions.fetchSize] The amount of rows to retrieve per page.
* @param {Number} [executionOptions.pageState] A Buffer instance or a string token representing the paging state.
* <p>When provided, the query will be executed starting from a given paging state.</p>
* @return {Promise<Result>} A Promise that resolves to a [Result]{@link module:mapping~Result} instance.
* @example <caption>Get user's videos</caption>
* const result = await videoMapper.find({ userId });
* for (let video of result) {
* console.log(video.name);
* }
* @example <caption>Get user's videos from a certain date</caption>
* videoMapper.find({ userId, addedDate: q.gte(date)});
* @example <caption>Get user's videos in reverse order</caption>
* videoMapper.find({ userId }, { orderBy: { addedDate: 'desc' }});
*/
find(doc, docInfo, executionOptions) {
if (executionOptions === undefined && typeof docInfo === 'string') {
executionOptions = docInfo;
docInfo = null;
}
return this._handler.getSelectExecutor(doc, docInfo, false)
.then(executor => executor(doc, docInfo, executionOptions));
}
/**
* Executes a SELECT query without a filter and returns the result as an iterable of documents.
* <p>
* This is only recommended to be used for tables with a limited amount of results. Otherwise, breaking up the
* token ranges on the client side should be used.
* </p>
* @param {Object} [docInfo] An object containing the additional document information.
* @param {Array<String>} [docInfo.fields] An Array containing the name of the properties that will be used in the
* SELECT cql statement generated, in order to restrict the amount of columns retrieved.
* @param {Object<String, String>} [docInfo.orderBy] An associative array containing the column names as key and
* the order string (asc or desc) as value used to set the order of the results server-side.
* @param {Number} [docInfo.limit] Restricts the result of the query to a maximum number of rows on the
* server.
* @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 {Number} [executionOptions.fetchSize] The mount of rows to retrieve per page.
* @param {Number} [executionOptions.pageState] A Buffer instance or a string token representing the paging state.
* <p>When provided, the query will be executed starting from a given paging state.</p>
* @return {Promise<Result>} A Promise that resolves to a [Result]{@link module:mapping~Result} instance.
*/
findAll(docInfo, executionOptions) {
if (executionOptions === undefined && typeof docInfo === 'string') {
executionOptions = docInfo;
docInfo = null;
}
const executor = this._handler.getSelectAllExecutor(docInfo);
return executor(docInfo, executionOptions);
}
/**
* Inserts a document.
* <p>
* When the model is mapped to multiple tables, it will insert a row in each table when all the primary keys
* are specified.
* </p>
* @param {Object} doc An object containing the properties to insert.
* @param {Object} [docInfo] An object containing the additional document information.
* @param {Array<String>} [docInfo.fields] An Array containing the name of the properties that will be used in the
* INSERT cql statements generated. If specified, it must include the columns to insert and the primary keys.
* @param {Number} [docInfo.ttl] Specifies an optional Time To Live (in seconds) for the inserted values.
* @param {Boolean} [docInfo.ifNotExists] When set, it only inserts if the row does not exist prior to the insertion.
* <p>Please note that using IF NOT EXISTS will incur a non negligible performance cost so this should be used
* sparingly.</p>
* @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>
* By default all generated INSERT statements are considered idempotent, except in the case of lightweight
* transactions. 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 {Number|Long} [executionOptions.timestamp] The default timestamp for the query in microseconds from the
* unix epoch (00:00:00, January 1st, 1970).
* <p>When provided, this will replace the client generated and the server side assigned timestamp.</p>
* @return {Promise<Result>} A Promise that resolves to a [Result]{@link module:mapping~Result} instance.
* @example <caption>Insert a video</caption>
* videoMapper.insert({ id, name });
*/
insert(doc, docInfo, executionOptions) {
if (executionOptions === undefined && typeof docInfo === 'string') {
executionOptions = docInfo;
docInfo = null;
}
return this._handler.getInsertExecutor(doc, docInfo)
.then(executor => executor(doc, docInfo, executionOptions));
}
/**
* Updates a document.
* <p>
* When the model is mapped to multiple tables, it will update a row in each table when all the primary keys
* are specified.
* </p>
* @param {Object} doc An object containing the properties to update.
* @param {Object} [docInfo] An object containing the additional document information.
* @param {Array<String>} [docInfo.fields] An Array containing the name of the properties that will be used in the
* UPDATE cql statements generated. If specified, it must include the columns to update and the primary keys.
* @param {Number} [docInfo.ttl] Specifies an optional Time To Live (in seconds) for the inserted values.
* @param {Boolean} [docInfo.ifExists] When set, it only updates if the row already exists on the server.
* <p>
* Please note that using IF conditions will incur a non negligible performance cost on the server-side so this
* should be used sparingly.
* </p>
* @param {Object} [docInfo.when] A document that act as the condition that has to be met for the UPDATE to occur.
* Use this property only in the case you want to specify a conditional clause for lightweight transactions (CAS).
* <p>
* Please note that using IF conditions will incur a non negligible performance cost on the server-side so this
* should be used sparingly.
* </p>
* @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 {Number|Long} [executionOptions.timestamp] The default timestamp for the query in microseconds from the
* unix epoch (00:00:00, January 1st, 1970).
* <p>When provided, this will replace the client generated and the server side assigned timestamp.</p>
* @return {Promise<Result>} A Promise that resolves to a [Result]{@link module:mapping~Result} instance.
* @example <caption>Update the name of a video</caption>
* videoMapper.update({ id, name });
*/
update(doc, docInfo, executionOptions) {
if (executionOptions === undefined && typeof docInfo === 'string') {
executionOptions = docInfo;
docInfo = null;
}
return this._handler.getUpdateExecutor(doc, docInfo)
.then(executor => executor(doc, docInfo, executionOptions));
}
/**
* Deletes a document.
* @param {Object} doc A document containing the primary keys values of the document to delete.
* @param {Object} [docInfo] An object containing the additional doc information.
* @param {Object} [docInfo.when] A document that act as the condition that has to be met for the DELETE to occur.
* Use this property only in the case you want to specify a conditional clause for lightweight transactions (CAS).
* When the CQL query is generated, this would be used to generate the `IF` clause.
* <p>
* Please note that using IF conditions will incur a non negligible performance cost on the server-side so this
* should be used sparingly.
* </p>
* @param {Boolean} [docInfo.ifExists] When set, it only issues the DELETE command if the row already exists on the
* server.
* <p>
* Please note that using IF conditions will incur a non negligible performance cost on the server-side so this
* should be used sparingly.
* </p>
* @param {Array<String>} [docInfo.fields] An Array containing the name of the properties that will be used in the
* DELETE cql statement generated. If specified, it must include the columns to delete and the primary keys.
* @param {Boolean} [docInfo.deleteOnlyColumns] Determines that, when more document properties are specified
* besides the primary keys, the generated DELETE statement should be used to delete some column values but leave
* the row. When this is enabled and more properties are specified, a DELETE statement will have the following form:
* "DELETE col1, col2 FROM table1 WHERE pk1 = ? AND pk2 = ?"
* @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>
* By default all generated DELETE statements are considered idempotent, except in the case of lightweight
* transactions. 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 {Number|Long} [executionOptions.timestamp] The default timestamp for the query in microseconds from the
* unix epoch (00:00:00, January 1st, 1970).
* <p>When provided, this will replace the client generated and the server side assigned timestamp.</p>
* @return {Promise<Result>} A Promise that resolves to a [Result]{@link module:mapping~Result} instance.
* @example <caption>Delete a video</caption>
* videoMapper.remove({ id });
*/
remove(doc, docInfo, executionOptions) {
if (executionOptions === undefined && typeof docInfo === 'string') {
executionOptions = docInfo;
docInfo = null;
}
return this._handler.getDeleteExecutor(doc, docInfo)
.then(executor => executor(doc, docInfo, executionOptions));
}
/**
* Uses the provided query and param getter function to execute a query and map the results.
* Gets a function that takes the document, executes the query and returns the mapped results.
* @param {String} query The query to execute.
* @param {Function} paramsHandler The function to execute to extract the parameters of a document.
* @param {Object|String} [executionOptions] When provided, the options for all executions generated with this
* method will use the provided options and it will not consider the executionOptions per call.
* @param {String} [executionOptions.executionProfile] The name of the execution profile.
* @param {Number} [executionOptions.fetchSize] Amount of rows to retrieve per page.
* @param {Boolean} [executionOptions.isIdempotent] Defines whether the query can be applied multiple times
* without changing the result beyond the initial application.
* @param {Number} [executionOptions.pageState] Buffer or string token representing the paging state.
* <p>When provided, the query will be executed starting from a given paging state.</p>
* @param {Number|Long} [executionOptions.timestamp] The default timestamp for the query in microseconds from the
* unix epoch (00:00:00, January 1st, 1970).
* <p>When provided, this will replace the client generated and the server side assigned timestamp.</p>
* @return {Function} Returns a function that takes the document and execution options as parameters and returns a
* Promise the resolves to a [Result]{@link module:mapping~Result} instance.
*/
mapWithQuery(query, paramsHandler, executionOptions) {
return this._handler.getExecutorFromQuery(query, paramsHandler, executionOptions);
}
}
module.exports = ModelMapper;

View File

@@ -0,0 +1,194 @@
/*
* 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 tableMappingsModule = require('./table-mappings');
const TableMappings = tableMappingsModule.TableMappings;
const DefaultTableMappings = tableMappingsModule.DefaultTableMappings;
/**
* Represents the parsed user information of the table mappings of a model.
* @ignore
*/
class ModelMappingInfo {
/**
* @param {String} keyspace
* @param {Array<{name, isView}>} tables
* @param {TableMappings} mappings
* @param {Map<String,ModelColumnInfo>} columns
*/
constructor(keyspace, tables, mappings, columns) {
this.keyspace = keyspace;
this.tables = tables;
this._mappings = mappings;
this._columns = columns;
// Define a map of column information per property name
/** @type {Map<String, ModelColumnInfo>} */
this._documentProperties = new Map();
for (const modelColumnInfo of columns.values()) {
this._documentProperties.set(modelColumnInfo.propertyName, modelColumnInfo);
}
}
getColumnName(propName) {
const modelColumnInfo = this._documentProperties.get(propName);
if (modelColumnInfo !== undefined) {
// There is an specific name transformation between the column name and the property name
return modelColumnInfo.columnName;
}
// Rely on the TableMappings (i.e. maybe there is a convention defined for this property)
return this._mappings.getColumnName(propName);
}
getPropertyName(columnName) {
const modelColumnInfo = this._columns.get(columnName);
if (modelColumnInfo !== undefined) {
// There is an specific name transformation between the column name and the property name
return modelColumnInfo.propertyName;
}
// Rely on the TableMappings (i.e. maybe there is a convention defined for this column)
return this._mappings.getPropertyName(columnName);
}
getFromModelFn(propName) {
const modelColumnInfo = this._documentProperties.get(propName);
return modelColumnInfo !== undefined ? modelColumnInfo.fromModel : null;
}
getToModelFn(columnName) {
const modelColumnInfo = this._columns.get(columnName);
return modelColumnInfo !== undefined ? modelColumnInfo.toModel : null;
}
newInstance() {
return this._mappings.newObjectInstance();
}
/**
* Parses the user options into a map of model names and ModelMappingInfo.
* @param {MappingOptions} options
* @param {String} currentKeyspace
* @returns {Map<String, ModelMappingInfo>}
*/
static parse(options, currentKeyspace) {
const result = new Map();
if (!options || !options.models) {
return result;
}
Object.keys(options.models).forEach(modelName => {
const modelOptions = options.models[modelName];
result.set(modelName, ModelMappingInfo._create(modelName, currentKeyspace, modelOptions));
});
return result;
}
static _create(modelName, currentKeyspace, modelOptions) {
if (!currentKeyspace && (!modelOptions || !modelOptions.keyspace)) {
throw new Error(
'You should specify the keyspace of the model in the MappingOptions when the Client is not using a keyspace');
}
if (!modelOptions) {
return ModelMappingInfo.createDefault(modelName, currentKeyspace);
}
let tables;
if (modelOptions.tables && modelOptions.tables.length > 0) {
tables = modelOptions.tables.map(item => {
const table = { name: null, isView: false };
if (typeof item === 'string') {
table.name = item;
} else if (item) {
table.name = item.name;
table.isView = !!item.isView;
}
if (!table.name) {
throw new Error(`Table name not specified for model '${modelName}'`);
}
return table;
});
} else {
tables = [ { name: modelName, isView: false }];
}
if (modelOptions.mappings && !(modelOptions.mappings instanceof TableMappings)) {
throw new Error('mappings should be an instance of TableMappings');
}
const columns = new Map();
if (modelOptions.columns !== null && typeof modelOptions.columns === 'object') {
Object.keys(modelOptions.columns).forEach(columnName => {
columns.set(columnName, ModelColumnInfo.parse(columnName, modelOptions.columns[columnName]));
});
}
return new ModelMappingInfo(
modelOptions.keyspace || currentKeyspace,
tables,
modelOptions.mappings || new DefaultTableMappings(),
columns
);
}
static createDefault(modelName, currentKeyspace) {
return new ModelMappingInfo(
currentKeyspace,
[ { name: modelName, isView: false }],
new DefaultTableMappings(),
new Map());
}
}
class ModelColumnInfo {
constructor(columnName, propertyName, toModel, fromModel) {
this.columnName = columnName;
this.propertyName = propertyName;
if (toModel && typeof toModel !== 'function') {
throw new TypeError(`toModel type for property '${propertyName}' should be a function (obtained ${
typeof toModel})`);
}
if (fromModel && typeof fromModel !== 'function') {
throw new TypeError(`fromModel type for property '${propertyName}' should be a function (obtained ${
typeof fromModel})`);
}
this.toModel = toModel;
this.fromModel = fromModel;
}
static parse(columnName, value) {
if (!value) {
return new ModelColumnInfo(columnName, columnName);
}
if (typeof value === 'string') {
return new ModelColumnInfo(columnName, value);
}
return new ModelColumnInfo(columnName, value.name || columnName, value.toModel, value.fromModel);
}
}
module.exports = ModelMappingInfo;

View File

@@ -0,0 +1,321 @@
/*
* 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 keyMatches = {
all: 1,
none: 0,
some: -1
};
/**
* Provides utility methods to choose the correct tables and views that should be included in a statement.
* @ignore
*/
class ObjectSelector {
/**
* Gets the table/view that should be used to execute the SELECT query.
* @param {Client} client
* @param {ModelMappingInfo} info
* @param {Boolean} allPKsDefined
* @param {Array} propertiesInfo
* @param {Array} fieldsInfo
* @param {Array<Array<String>>} orderByColumns
* @return {Promise<String>} A promise that resolves to a table names.
*/
static getForSelect(client, info, allPKsDefined, propertiesInfo, fieldsInfo, orderByColumns) {
return Promise.all(
info.tables.map(t => {
if (t.isView) {
return client.metadata.getMaterializedView(info.keyspace, t.name);
}
return client.metadata.getTable(info.keyspace, t.name);
}))
.then(tables => {
for (let i = 0; i < tables.length; i++) {
const table = tables[i];
if (table === null) {
throw new Error(`Table "${info.tables[i].name}" could not be retrieved`);
}
if (keysAreIncluded(table.partitionKeys, propertiesInfo) !== keyMatches.all) {
// Not all the partition keys are covered
continue;
}
if (allPKsDefined) {
if (keysAreIncluded(table.clusteringKeys, propertiesInfo) !== keyMatches.all) {
// All clustering keys should be included as allPKsDefined flag is set
continue;
}
}
if (propertiesInfo.length > table.partitionKeys.length) {
// Check that the Where clause is composed by partition and clustering keys
const allPropertiesArePrimaryKeys = propertiesInfo
.reduce(
(acc, p) => acc && (
contains(table.partitionKeys, c => c.name === p.columnName) ||
contains(table.clusteringKeys, c => c.name === p.columnName)
),
true);
if (!allPropertiesArePrimaryKeys) {
continue;
}
}
// All fields must be contained
const containsAllFields = fieldsInfo
.reduce((acc, p) => acc && table.columnsByName[p.columnName] !== undefined, true);
if (!containsAllFields) {
continue;
}
// CQL:
// - "ORDER BY" is currently only supported on the clustered columns of the PRIMARY KEY
// - "ORDER BY" currently only support the ordering of columns following their declared order in
// the PRIMARY KEY
//
// In the mapper, we validate that the ORDER BY columns appear in the same order as in the clustering keys
const containsAllOrderByColumns = orderByColumns
.reduce((acc, order, index) => {
if (!acc) {
return false;
}
const ck = table.clusteringKeys[index];
return ck && ck.name === order[0];
}, true);
if (!containsAllOrderByColumns) {
continue;
}
return table.name;
}
let message = `No table matches the filter (${allPKsDefined ? 'all PKs have to be specified' : 'PKs'}): [${
propertiesInfo.map(p => p.columnName)}]`;
if (fieldsInfo.length > 0) {
message += `; fields: [${fieldsInfo.map(p => p.columnName)}]`;
}
if (orderByColumns.length > 0) {
message += `; orderBy: [${orderByColumns.map(item => item[0])}]`;
}
throw new Error(message);
});
}
/** Returns the name of the first table */
static getForSelectAll(info) {
return info.tables[0].name;
}
/**
* Gets the tables that should be used to execute the INSERT query.
* @param {Client} client
* @param {ModelMappingInfo} info
* @param {Array} propertiesInfo
* @return {Promise<Array<TableMetadata>>} A promise that resolves to an Array of tables.
*/
static getForInsert(client, info, propertiesInfo) {
return Promise.all(info.tables.filter(t => !t.isView).map(t => client.metadata.getTable(info.keyspace, t.name)))
.then(tables => {
const filteredTables = tables
.filter((table, i) => {
if (table === null) {
throw new Error(`Table "${info.tables[i].name}" could not be retrieved`);
}
if (keysAreIncluded(table.partitionKeys, propertiesInfo) !== keyMatches.all) {
// Not all the partition keys are covered
return false;
}
const clusteringKeyMatches = keysAreIncluded(table.clusteringKeys, propertiesInfo);
// All clustering keys should be included or it can be inserting a static column value
if (clusteringKeyMatches === keyMatches.all) {
return true;
}
if (clusteringKeyMatches === keyMatches.some) {
return false;
}
const staticColumns = staticColumnCount(table);
return propertiesInfo.length === table.partitionKeys.length + staticColumns && staticColumns > 0;
});
if (filteredTables.length === 0) {
throw new Error(`No table matches (all PKs have to be specified) fields: [${
propertiesInfo.map(p => p.columnName)}]`);
}
return filteredTables;
});
}
/**
* Gets the tables that should be used to execute the UPDATE query.
* @param {Client} client
* @param {ModelMappingInfo} info
* @param {Array} propertiesInfo
* @param {Array} when
* @return {Promise<Array<TableMetadata>>} A promise that resolves to an Array of tables.
*/
static getForUpdate(client, info, propertiesInfo, when) {
return Promise.all(info.tables.filter(t => !t.isView).map(t => client.metadata.getTable(info.keyspace, t.name)))
.then(tables => {
const filteredTables = tables
.filter((table, i) => {
if (table === null) {
throw new Error(`Table "${info.tables[i].name}" could not be retrieved`);
}
if (keysAreIncluded(table.partitionKeys, propertiesInfo) !== keyMatches.all) {
// Not all the partition keys are covered
return false;
}
const clusteringKeyMatches = keysAreIncluded(table.clusteringKeys, propertiesInfo);
// All clustering keys should be included or it can be updating a static column value
if (clusteringKeyMatches === keyMatches.some) {
return false;
}
if (clusteringKeyMatches === keyMatches.none && !hasStaticColumn(table)) {
return false;
}
const applicableColumns = propertiesInfo
.reduce((acc, p) => acc + (table.columnsByName[p.columnName] !== undefined ? 1 : 0), 0);
if (applicableColumns <= table.partitionKeys.length + table.clusteringKeys.length) {
if (!hasStaticColumn(table) || applicableColumns <= table.partitionKeys.length) {
// UPDATE statement does not contain columns to SET
return false;
}
}
// "when" conditions should be contained in the table
return when.reduce((acc, p) => acc && table.columnsByName[p.columnName] !== undefined, true);
});
if (filteredTables.length === 0) {
let message = `No table matches (all PKs and columns to set have to be specified) fields: [${
propertiesInfo.map(p => p.columnName)}]`;
if (when.length > 0) {
message += `; condition: [${when.map(p => p.columnName)}]`;
}
throw new Error(message);
}
return filteredTables;
});
}
/**
* Gets the tables that should be used to execute the DELETE query.
* @param {Client} client
* @param {ModelMappingInfo} info
* @param {Array} propertiesInfo
* @param {Array} when
* @return {Promise<Array<TableMetadata>>} A promise that resolves to an Array of tables.
*/
static getForDelete(client, info, propertiesInfo, when) {
return Promise.all(info.tables.filter(t => !t.isView).map(t => client.metadata.getTable(info.keyspace, t.name)))
.then(tables => {
const filteredTables = tables
.filter((table, i) => {
if (table === null) {
throw new Error(`Table "${info.tables[i].name}" could not be retrieved`);
}
// All partition and clustering keys from the table should be included in the document
const keyNames = table.partitionKeys.concat(table.clusteringKeys).map(k => k.name);
const columns = propertiesInfo.map(p => p.columnName);
for (let i = 0; i < keyNames.length; i++) {
if (columns.indexOf(keyNames[i]) === -1) {
return false;
}
}
// "when" conditions should be contained in the table
return when.reduce((acc, p) => acc && table.columnsByName[p.columnName] !== undefined, true);
});
if (filteredTables.length === 0) {
let message = `No table matches (all PKs have to be specified) fields: [${
propertiesInfo.map(p => p.columnName)}]`;
if (when.length > 0) {
message += `; condition: [${when.map(p => p.columnName)}]`;
}
throw new Error(message);
}
return filteredTables;
});
}
}
function contains(arr, fn) {
return arr.filter(fn).length > 0;
}
/**
* Returns the amount of matches for a given key
* @private
* @param {Array} keys
* @param {Array} propertiesInfo
*/
function keysAreIncluded(keys, propertiesInfo) {
if (keys.length === 0) {
return keyMatches.all;
}
// Filtering by name might look slow / ineffective to using hash maps
// but we expect `keys` and `propertiesInfo` to contain only few items
const matches = propertiesInfo.reduce((acc, p) => acc + (contains(keys, k => p.columnName === k.name) ? 1 : 0), 0);
if (matches === 0) {
return keyMatches.none;
}
return matches === keys.length ? keyMatches.all : keyMatches.some;
}
function hasStaticColumn(table) {
return staticColumnCount(table) > 0;
}
function staticColumnCount(table) {
return table.columns.reduce((acc, column) => acc + (column.isStatic ? 1 : 0), 0);
}
module.exports = ObjectSelector;

154
node_modules/cassandra-driver/lib/mapping/q.js generated vendored Normal file
View File

@@ -0,0 +1,154 @@
/*
* 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 errors = require('../errors');
/**
* Represents a CQL query operator, like >=, IN, <, ...
* @ignore
*/
class QueryOperator {
/**
* Creates a new instance of <code>QueryOperator</code>.
* @param {String} key
* @param value
* @param [hasChildValues]
* @param [isInOperator]
*/
constructor(key, value, hasChildValues, isInOperator) {
/**
* The CQL key representing the operator
* @type {string}
*/
this.key = key;
/**
* The value to be used as parameter.
*/
this.value = value;
/**
* Determines whether a query operator can have child values or operators (AND, OR)
*/
this.hasChildValues = hasChildValues;
/**
* Determines whether this instance represents CQL "IN" operator.
*/
this.isInOperator = isInOperator;
}
}
/**
* Represents a CQL assignment operation, like col = col + x.
* @ignore
*/
class QueryAssignment {
constructor(sign, value, inverted) {
/**
* Gets the sign of the assignment operation.
*/
this.sign = sign;
/**
* Gets the value to be assigned.
*/
this.value = value;
/**
* Determines whether the assignment should be inverted (prepends), e.g: col = x + col
* @type {boolean}
*/
this.inverted = !!inverted;
}
}
/**
* Contains functions that represents operators in a query.
* @alias module:mapping~q
* @type {Object}
* @property {function} in_ Represents the CQL operator "IN".
* @property {function} gt Represents the CQL operator greater than ">".
* @property {function} gte Represents the CQL operator greater than or equals to ">=" .
* @property {function} lt Represents the CQL operator less than "<" .
* @property {function} lte Represents the CQL operator less than or equals to "<=" .
* @property {function} notEq Represents the CQL operator not equals to "!=" .
* @property {function} and When applied to a property, it represents two CQL conditions on the same column separated
* by the logical AND operator, e.g: "col1 >= x col < y"
* @property {function} incr Represents the CQL increment assignment used for counters, e.g: "col = col + x"
* @property {function} decr Represents the CQL decrement assignment used for counters, e.g: "col = col - x"
* @property {function} append Represents the CQL append assignment used for collections, e.g: "col = col + x"
* @property {function} prepend Represents the CQL prepend assignment used for lists, e.g: "col = x + col"
* @property {function} remove Represents the CQL remove assignment used for collections, e.g: "col = col - x"
*/
const q = {
in_: function in_(arr) {
if (!Array.isArray(arr)) {
throw new errors.ArgumentError('IN operator supports only Array values');
}
return new QueryOperator('IN', arr, false, true);
},
gt: function gt(value) {
return new QueryOperator('>', value);
},
gte: function gte(value) {
return new QueryOperator('>=', value);
},
lt: function lt(value) {
return new QueryOperator('<', value);
},
lte: function lte(value) {
return new QueryOperator('<=', value);
},
notEq: function notEq(value) {
return new QueryOperator('!=', value);
},
and: function (condition1, condition2) {
return new QueryOperator('AND', [ condition1, condition2 ], true);
},
incr: function incr(value) {
return new QueryAssignment('+', value);
},
decr: function decr(value) {
return new QueryAssignment('-', value);
},
append: function append(value) {
return new QueryAssignment('+', value);
},
prepend: function prepend(value) {
return new QueryAssignment('+', value, true);
},
remove: function remove(value) {
return new QueryAssignment('-', value);
}
};
exports.q = q;
exports.QueryAssignment = QueryAssignment;
exports.QueryOperator = QueryOperator;

View File

@@ -0,0 +1,446 @@
/*
* 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 vm = require('vm');
const qModule = require('./q');
const QueryOperator = qModule.QueryOperator;
const QueryAssignment = qModule.QueryAssignment;
const types = require('../types');
const dataTypes = types.dataTypes;
const vmFileName = 'gen-param-getter.js';
/**
* Provides methods to generate a query and parameter handlers.
* @ignore
*/
class QueryGenerator {
/**
* Gets the SELECT query given the doc.
* @param {String} tableName
* @param {String} keyspace
* @param {Array} propertiesInfo
* @param {Array} fieldsInfo
* @param {Array} orderByColumns
* @param {Number|undefined} limit
* @return {string}
*/
static getSelect(tableName, keyspace, propertiesInfo, fieldsInfo, orderByColumns, limit) {
let query = 'SELECT ';
query += fieldsInfo.length > 0 ? fieldsInfo.map(p => p.columnName).join(', ') : '*';
query += ` FROM ${keyspace}.${tableName}`;
if (propertiesInfo.length > 0) {
query += ' WHERE ';
query += QueryGenerator._getConditionWithOperators(propertiesInfo);
}
if (orderByColumns.length > 0) {
query += ' ORDER BY ';
query += orderByColumns.map(order => order[0] + ' ' + order[1]).join(', ');
}
if (typeof limit === 'number') {
query += ' LIMIT ?';
}
return query;
}
static selectParamsGetter(propertiesInfo, limit) {
let scriptText = '(function getParametersSelect(doc, docInfo, mappingInfo) {\n';
scriptText += ' return [';
scriptText += QueryGenerator._valueGetterExpression(propertiesInfo);
if (typeof limit === 'number') {
if (propertiesInfo.length > 0) {
scriptText += ', ';
}
scriptText += `docInfo['limit']`;
}
// Finish return statement
scriptText += '];\n})';
const script = new vm.Script(scriptText, { filename: vmFileName });
return script.runInThisContext();
}
/**
* Gets the INSERT query and function to obtain the parameters, given the doc.
* @param {TableMetadata} table
* @param {String} keyspace
* @param {Array} propertiesInfo
* @param {Object} docInfo
* @param {Boolean|undefined} ifNotExists
* @return {{query: String, paramsGetter: Function, isIdempotent: Boolean}}
*/
static getInsert(table, keyspace, propertiesInfo, docInfo, ifNotExists) {
const ttl = docInfo && docInfo.ttl;
// Not all columns are contained in the table
const filteredPropertiesInfo = propertiesInfo
.filter(pInfo => table.columnsByName[pInfo.columnName] !== undefined);
return ({
query: QueryGenerator._getInsertQuery(table.name, keyspace, filteredPropertiesInfo, ifNotExists, ttl),
paramsGetter: QueryGenerator._insertParamsGetter(filteredPropertiesInfo, docInfo),
isIdempotent: !ifNotExists
});
}
/**
* Gets the query for an insert statement.
* @param {String} tableName
* @param {String} keyspace
* @param {Array} propertiesInfo
* @param {Boolean} ifNotExists
* @param {Number|undefined} ttl
* @return {String}
*/
static _getInsertQuery(tableName, keyspace, propertiesInfo, ifNotExists, ttl) {
let query = `INSERT INTO ${keyspace}.${tableName} (`;
query += propertiesInfo.map(pInfo => pInfo.columnName).join(', ');
query += ') VALUES (';
query += propertiesInfo.map(() => '?').join(', ');
query += ')';
if (ifNotExists === true) {
query += ' IF NOT EXISTS';
}
if (typeof ttl === 'number') {
query += ' USING TTL ?';
}
return query;
}
static _insertParamsGetter(propertiesInfo, docInfo) {
let scriptText = '(function getParametersInsert(doc, docInfo, mappingInfo) {\n';
scriptText += ' return [';
scriptText += QueryGenerator._valueGetterExpression(propertiesInfo);
if (docInfo && typeof docInfo.ttl === 'number') {
scriptText += `, docInfo['ttl']`;
}
// Finish return statement
scriptText += '];\n})';
const script = new vm.Script(scriptText, { filename: vmFileName });
return script.runInThisContext();
}
/**
* Gets the UPDATE query and function to obtain the parameters, given the doc.
* @param {TableMetadata} table
* @param {String} keyspace
* @param {Array} propertiesInfo
* @param {Object} docInfo
* @param {Array} when
* @param {Boolean|undefined} ifExists
* @return {{query: String, paramsGetter: Function, isIdempotent: Boolean, isCounter}}
*/
static getUpdate(table, keyspace, propertiesInfo, docInfo, when, ifExists) {
const ttl = docInfo && docInfo.ttl;
const primaryKeys = new Set(table.partitionKeys.concat(table.clusteringKeys).map(c => c.name));
let isIdempotent = true;
let isCounter = false;
// Not all columns are contained in the table
const filteredPropertiesInfo = propertiesInfo.filter(pInfo => {
const column = table.columnsByName[pInfo.columnName];
if (column === undefined) {
return false;
}
if (column.type.code === dataTypes.list && pInfo.value instanceof QueryAssignment) {
// Its not idempotent when list append/prepend
isIdempotent = false;
} else if (column.type.code === dataTypes.counter) {
// Any update on a counter table is not idempotent
isIdempotent = false;
isCounter = true;
}
return true;
});
return {
query: QueryGenerator._getUpdateQuery(
table.name, keyspace, primaryKeys, filteredPropertiesInfo, when, ifExists, ttl),
isIdempotent: isIdempotent && when.length === 0 && !ifExists,
paramsGetter: QueryGenerator._updateParamsGetter(primaryKeys, filteredPropertiesInfo, when, ttl),
isCounter
};
}
/**
* Gets the query for an UPDATE statement.
* @param {String} tableName
* @param {String} keyspace
* @param {Set} primaryKeys
* @param {Array} propertiesInfo
* @param {Object} when
* @param {Boolean} ifExists
* @param {Number|undefined} ttl
*/
static _getUpdateQuery(tableName, keyspace, primaryKeys, propertiesInfo, when, ifExists, ttl) {
let query = `UPDATE ${keyspace}.${tableName} `;
if (typeof ttl === 'number') {
query += 'USING TTL ? ';
}
query += 'SET ';
query += propertiesInfo
.filter(p => !primaryKeys.has(p.columnName))
.map(p => {
if (p.value instanceof QueryAssignment) {
if (p.value.inverted) {
// e.g: prepend "col1 = ? + col1"
return `${p.columnName} = ? ${p.value.sign} ${p.columnName}`;
}
// e.g: increment "col1 = col1 + ?"
return `${p.columnName} = ${p.columnName} ${p.value.sign} ?`;
}
return p.columnName + ' = ?';
})
.join(', ');
query += ' WHERE ';
query += propertiesInfo.filter(p => primaryKeys.has(p.columnName)).map(p => p.columnName + ' = ?').join(' AND ');
if (ifExists === true) {
query += ' IF EXISTS';
}
else if (when.length > 0) {
query += ' IF ' + QueryGenerator._getConditionWithOperators(when);
}
return query;
}
/**
* Returns a function to obtain the parameter values from a doc for an UPDATE statement.
* @param {Set} primaryKeys
* @param {Array} propertiesInfo
* @param {Array} when
* @param {Number|undefined} ttl
* @returns {Function}
*/
static _updateParamsGetter(primaryKeys, propertiesInfo, when, ttl) {
let scriptText = '(function getParametersUpdate(doc, docInfo, mappingInfo) {\n';
scriptText += ' return [';
if (typeof ttl === 'number') {
scriptText += `docInfo['ttl'], `;
}
// Assignment clause
scriptText += QueryGenerator._assignmentGetterExpression(propertiesInfo.filter(p => !primaryKeys.has(p.columnName)));
scriptText += ', ';
// Where clause
scriptText += QueryGenerator._valueGetterExpression(propertiesInfo.filter(p => primaryKeys.has(p.columnName)));
// Condition clause
if (when.length > 0) {
scriptText += ', ' + QueryGenerator._valueGetterExpression(when, 'docInfo.when');
}
// Finish return statement
scriptText += '];\n})';
const script = new vm.Script(scriptText, { filename: vmFileName });
return script.runInThisContext();
}
/**
* Gets the DELETE query and function to obtain the parameters, given the doc.
* @param {TableMetadata} table
* @param {String} keyspace
* @param {Array} propertiesInfo
* @param {Object} docInfo
* @param {Array} when
* @param {Boolean|undefined} ifExists
* @return {{query: String, paramsGetter: Function, isIdempotent}}
*/
static getDelete(table, keyspace, propertiesInfo, docInfo, when, ifExists) {
const deleteOnlyColumns = docInfo && docInfo.deleteOnlyColumns;
const primaryKeys = new Set(table.partitionKeys.concat(table.clusteringKeys).map(c => c.name));
const filteredPropertiesInfo = propertiesInfo
.filter(pInfo => table.columnsByName[pInfo.columnName] !== undefined);
return ({
query: QueryGenerator._getDeleteQuery(
table.name, keyspace, primaryKeys, filteredPropertiesInfo, when, ifExists, deleteOnlyColumns),
paramsGetter: QueryGenerator._deleteParamsGetter(primaryKeys, filteredPropertiesInfo, when),
isIdempotent: when.length === 0 && !ifExists
});
}
/**
* Gets the query for an UPDATE statement.
* @param {String} tableName
* @param {String} keyspace
* @param {Set} primaryKeys
* @param {Array} propertiesInfo
* @param {Array} when
* @param {Boolean} ifExists
* @param {Boolean} deleteOnlyColumns
* @private
* @return {String}
*/
static _getDeleteQuery(tableName, keyspace, primaryKeys, propertiesInfo, when, ifExists, deleteOnlyColumns) {
let query = 'DELETE';
if (deleteOnlyColumns) {
const columnsToDelete = propertiesInfo.filter(p => !primaryKeys.has(p.columnName))
.map(p => p.columnName)
.join(', ');
if (columnsToDelete !== '') {
query += ' ' + columnsToDelete;
}
}
query += ` FROM ${keyspace}.${tableName} WHERE `;
query += propertiesInfo.filter(p => primaryKeys.has(p.columnName)).map(p => p.columnName + ' = ?').join(' AND ');
if (ifExists === true) {
query += ' IF EXISTS';
}
else if (when.length > 0) {
query += ' IF ' + QueryGenerator._getConditionWithOperators(when);
}
return query;
}
/**
* Returns a function to obtain the parameter values from a doc for an UPDATE statement.
* @param {Set} primaryKeys
* @param {Array} propertiesInfo
* @param {Array} when
* @returns {Function}
*/
static _deleteParamsGetter(primaryKeys, propertiesInfo, when) {
let scriptText = '(function getParametersDelete(doc, docInfo, mappingInfo) {\n';
scriptText += ' return [';
// Where clause
scriptText += QueryGenerator._valueGetterExpression(propertiesInfo.filter(p => primaryKeys.has(p.columnName)));
// Condition clause
if (when.length > 0) {
scriptText += ', ' + QueryGenerator._valueGetterExpression(when, 'docInfo.when');
}
// Finish return statement
scriptText += '];\n})';
const script = new vm.Script(scriptText, { filename: vmFileName });
return script.runInThisContext();
}
/**
* Gets a string containing the doc properties to get.
* @param {Array} propertiesInfo
* @param {String} [objectName='doc']
* @return {string}
* @private
*/
static _valueGetterExpression(propertiesInfo, objectName) {
objectName = objectName || 'doc';
return propertiesInfo
.map(p =>
QueryGenerator._valueGetterSingle(`${objectName}['${p.propertyName}']`, p.propertyName, p.value, p.fromModel))
.join(', ');
}
static _valueGetterSingle(prefix, propName, value, fromModelFn) {
let valueGetter = prefix;
if (value instanceof QueryOperator) {
if (value.hasChildValues) {
return `${QueryGenerator._valueGetterSingle(`${prefix}.value[0]`, propName, value.value[0], fromModelFn)}` +
`, ${QueryGenerator._valueGetterSingle(`${prefix}.value[1]`, propName, value.value[1], fromModelFn)}`;
}
valueGetter = `${prefix}.value`;
if (value.isInOperator && fromModelFn) {
// Transform each individual value
return `${valueGetter}.map(v => ${QueryGenerator._getMappingFunctionCall(propName, 'v')})`;
}
}
return !fromModelFn ? valueGetter : QueryGenerator._getMappingFunctionCall(propName, valueGetter);
}
/**
* Gets a string containing the doc properties to SET, considering QueryAssignment instances.
* @param {Array} propertiesInfo
* @param {String} [prefix='doc']
* @return {string}
* @private
*/
static _assignmentGetterExpression(propertiesInfo, prefix) {
prefix = prefix || 'doc';
return propertiesInfo
.map(p => {
const valueGetter = `${prefix}['${p.propertyName}']${p.value instanceof QueryAssignment ? '.value' : ''}`;
if (p.fromModel) {
return QueryGenerator._getMappingFunctionCall(p.propertyName, valueGetter);
}
return valueGetter;
})
.join(', ');
}
static _getConditionWithOperators(propertiesInfo) {
return propertiesInfo
.map(p => QueryGenerator._getSingleCondition(p.columnName, p.value))
.join(' AND ');
}
static _getMappingFunctionCall(propName, valueGetter) {
return `mappingInfo.getFromModelFn('${propName}')(${valueGetter})`;
}
static _getSingleCondition(columnName, value) {
if (value instanceof QueryOperator) {
if (value.hasChildValues) {
return `${QueryGenerator._getSingleCondition(columnName, value.value[0])}` +
` ${value.key} ${QueryGenerator._getSingleCondition(columnName, value.value[1])}`;
}
return `${columnName} ${value.key} ?`;
}
return `${columnName} = ?`;
}
}
module.exports = QueryGenerator;

View File

@@ -0,0 +1,112 @@
/*
* 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 vm = require('vm');
const utils = require('../utils');
const types = require('../types');
/**
* @ignore
*/
class ResultMapper {
/**
* Gets a generated function to adapt the row to a document.
* @param {ModelMappingInfo} info
* @param {ResultSet} rs
* @returns {Function}
*/
static getSelectAdapter(info, rs) {
const columns = rs.columns;
if (!columns) {
throw new Error('Expected ROWS result obtained VOID');
}
let scriptText = '(function rowAdapter(row, info) {\n' +
' const item = info.newInstance();\n';
for (const c of columns) {
scriptText += ` item['${info.getPropertyName(c.name)}'] = `;
if (!info.getToModelFn(c.name)) {
scriptText += `row['${c.name}'];\n`;
} else {
scriptText += `info.getToModelFn('${c.name}')(row['${c.name}']);\n`;
}
}
scriptText += ' return item;\n})';
const script = new vm.Script(scriptText, { filename: 'gen-result-mapper.js'});
return script.runInThisContext();
}
/**
* Gets a function used to adapt VOID results or conditional updates.
* @param {ResultSet} rs
* @returns {Function}
*/
static getMutationAdapter(rs) {
if (rs.columns === null) {
// VOID result
return utils.noop;
}
if (
rs.columns.length === 1 && rs.columns[0].name === '[applied]' &&
rs.columns[0].type.code === types.dataTypes.boolean) {
return utils.noop;
}
return ResultMapper._getConditionalRowAdapter(rs);
}
static _getConditionalRowAdapter(rs) {
return (function conditionalRowAdapter(row, info) {
const item = info.newInstance();
// Skip the first column ("[applied]")
for (let i = 1; i < rs.columns.length; i++) {
const c = rs.columns[i];
item[info.getPropertyName(c.name)] = row[c.name];
}
return item;
});
}
/**
* @param {ModelMappingInfo} info
* @param {ResultSet} rs
* @returns {{canCache: Boolean, fn: Function}}
*/
static getCustomQueryAdapter(info, rs) {
if (rs.columns === null || rs.columns.length === 0) {
// VOID result
return { canCache: true, fn: utils.noop };
}
if (rs.columns[0].name === '[applied]' && rs.columns[0].type.code === types.dataTypes.boolean) {
// Conditional update results adapter functions should not be cached
return { canCache: false, fn: ResultMapper._getConditionalRowAdapter(rs) };
}
return { canCache: true, fn: ResultMapper.getSelectAdapter(info, rs) };
}
}
module.exports = ResultMapper;

136
node_modules/cassandra-driver/lib/mapping/result.js generated vendored Normal file
View File

@@ -0,0 +1,136 @@
/*
* 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 util = require('util');
const utils = require('../utils');
const inspectMethod = util.inspect.custom || 'inspect';
/**
* Represents the result of an execution as an iterable of objects in the Mapper.
* @alias module:mapping~Result
*/
class Result {
/**
* Creates a new instance of Result.
* @param {ResultSet} rs
* @param {ModelMappingInfo} info
* @param {Function} rowAdapter
*/
constructor(rs, info, rowAdapter) {
this._rs = rs;
this._info = info;
this._rowAdapter = rowAdapter;
/**
* When there is a single cell containing the result of the a LWT operation, hide the result from the user.
* @private
*/
this._isEmptyLwt = (rs.columns !== null
&& rs.columns.length === 1 && this._rs.rowLength === 1 && rs.columns[0].name === '[applied]');
/**
* Gets the amount of the documents contained in this Result instance.
* <p>
* When the results are paged, it returns the length of the current paged results not the total amount of
* rows in the table matching the query.
* </p>
* @type {Number}
*/
this.length = this._isEmptyLwt ? 0 : (rs.rowLength || 0);
/**
* A string token representing the current page state of query.
* <p>
* When provided, it can be used in the following executions to continue paging and retrieve the remained of the
* result for the query.
* </p>
* @type {String}
* @default null
*/
this.pageState = rs.pageState;
}
/**
* When this instance is the result of a conditional update query, it returns whether it was successful.
* Otherwise, it returns <code>true</code>.
* <p>
* For consistency, this method always returns <code>true</code> for non-conditional queries (although there is
* no reason to call the method in that case). This is also the case for conditional DDL statements
* (CREATE KEYSPACE... IF NOT EXISTS, CREATE TABLE... IF NOT EXISTS), for which the server doesn't return
* information whether it was applied or not.
* </p>
*/
wasApplied() {
return this._rs.wasApplied();
}
/**
* Gets the first document in this result or null when the result is empty.
*/
first() {
if (!this._rs.rowLength || this._isEmptyLwt) {
return null;
}
return this._rowAdapter(this._rs.rows[0], this._info);
}
/**
* Returns a new Iterator object that contains the document values.
*/
*[Symbol.iterator]() {
if (this._isEmptyLwt) {
// Empty iterator
return;
}
for (let i = 0; i < this._rs.rows.length; i++) {
yield this._rowAdapter(this._rs.rows[i], this._info);
}
}
/**
* Converts the current instance to an Array of documents.
* @return {Array<Object>}
*/
toArray() {
if (this._isEmptyLwt || !this._rs.rows) {
return utils.emptyArray;
}
return this._rs.rows.map(row => this._rowAdapter(row, this._info));
}
/**
* Executes a provided function once per result element.
* @param {Function} callback Function to execute for each element, taking two arguments: currentValue and index.
* @param {Object} [thisArg] Value to use as <code>this</code> when executing callback.
*/
forEach(callback, thisArg) {
let index = 0;
thisArg = thisArg || this;
for (const doc of this) {
callback.call(thisArg, doc, index++);
}
}
[inspectMethod]() {
return this.toArray();
}
}
module.exports = Result;

View File

@@ -0,0 +1,122 @@
/*
* 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';
/**
* Contains a set of methods to represent a row into a document and a document into a row.
* @alias module:mapping~TableMappings
* @interface
*/
class TableMappings {
/**
* Method that is called by the mapper to create the instance of the document.
* @return {Object}
*/
newObjectInstance() {
return {};
}
/**
* Gets the name of the column based on the document property name.
* @param {String} propName The name of the property.
* @returns {String}
*/
getColumnName(propName) {
return propName;
}
/**
* Gets the name of the document property based on the column name.
* @param {String} columnName The name of the column.
* @returns {String}
*/
getPropertyName(columnName) {
return columnName;
}
}
/**
* A [TableMappings]{@link module:mapping~TableMappings} implementation that converts CQL column names in all-lowercase
* identifiers with underscores (snake case) to camel case (initial lowercase letter) property names.
* <p>
* The conversion is performed without any checks for the source format, you should make sure that the source
* format is snake case for CQL identifiers and camel case for properties.
* </p>
* @alias module:mapping~UnderscoreCqlToCamelCaseMappings
* @implements {module:mapping~TableMappings}
*/
class UnderscoreCqlToCamelCaseMappings extends TableMappings {
/**
* Creates a new instance of {@link UnderscoreCqlToCamelCaseMappings}
*/
constructor() {
super();
}
/**
* Converts a property name in camel case to snake case.
* @param {String} propName Name of the property to convert to snake case.
* @return {String}
*/
getColumnName(propName) {
return propName.replace(/[a-z][A-Z]/g, (match, offset) => match.charAt(0) + '_' + match.charAt(1)).toLowerCase();
}
/**
* Converts a column name in snake case to camel case.
* @param {String} columnName The column name to convert to camel case.
* @return {String}
*/
getPropertyName(columnName) {
return columnName.replace(/_[a-z]/g, (match, offset) => ((offset === 0) ? match : match.substr(1).toUpperCase()));
}
}
/**
* Default implementation of [TableMappings]{@link module:mapping~TableMappings} that doesn't perform any conversion.
* @alias module:mapping~DefaultTableMappings
* @implements {module:mapping~TableMappings}
*/
class DefaultTableMappings extends TableMappings {
/**
* Creates a new instance of {@link DefaultTableMappings}.
*/
constructor() {
super();
}
/** @override */
getColumnName(propName) {
return super.getColumnName(propName);
}
/** @override */
getPropertyName(columnName) {
return super.getPropertyName(columnName);
}
/**
* Creates a new object instance, using object initializer.
*/
newObjectInstance() {
return super.newObjectInstance();
}
}
exports.TableMappings = TableMappings;
exports.UnderscoreCqlToCamelCaseMappings = UnderscoreCqlToCamelCaseMappings;
exports.DefaultTableMappings = DefaultTableMappings;

151
node_modules/cassandra-driver/lib/mapping/tree.js generated vendored Normal file
View File

@@ -0,0 +1,151 @@
/*
* 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 EventEmitter = require('events');
/**
* Represents a tree node where the key is composed by 1 or more strings.
* @ignore
*/
class Node extends EventEmitter {
/**
* Creates a new instance of {@link Node}.
* @param {Array<String>} key
* @param {Object} value
* @param {Array} [edges]
*/
constructor(key, value, edges) {
super();
this.key = key;
this.value = value;
this.edges = edges || [];
}
}
/**
* A radix tree where each node contains a key, a value and edges.
* @ignore
*/
class Tree extends Node {
constructor() {
super([], null);
this.length = 0;
}
/**
* Gets the existing item in the tree or creates a new one with the value provided by valueHandler
* @param {Iterator} keyIterator
* @param {Function} valueHandler
* @return {Object}
*/
getOrCreate(keyIterator, valueHandler) {
if (typeof keyIterator.next !== 'function') {
keyIterator = keyIterator[Symbol.iterator]();
}
let node = this;
let isMatch = false;
let item = keyIterator.next();
while (true) {
let newBranch;
// Check node keys at position 1 and above
for (let i = 1; i < node.key.length; i++) {
if (item.done || node.key[i] !== item.value) {
// We should branch out
newBranch = this._createBranch(node, i, item.done, valueHandler);
break;
}
item = keyIterator.next();
}
if (item.done) {
isMatch = true;
break;
}
if (newBranch !== undefined) {
break;
}
const edges = node.edges;
let nextNode;
for (let i = 0; i < edges.length; i++) {
const e = edges[i];
if (e.key[0] === item.value) {
// its a match
nextNode = e;
item = keyIterator.next();
break;
}
}
if (nextNode === undefined) {
// Current node is the root for a new leaf
break;
}
else {
node = nextNode;
}
}
if (!isMatch) {
// Create using "node" as the root
const value = valueHandler();
node.edges.push(new Node(iteratorToArray(item.value, keyIterator), value));
this._onItemAdded();
return value;
}
if (node.value === null && node.edges.length > 0) {
node.value = valueHandler();
}
return node.value;
}
_createBranch(node, index, useNewValue, valueHandler) {
const newBranch = new Node(node.key.slice(index), node.value, node.edges);
node.key = node.key.slice(0, index);
node.edges = [ newBranch ];
if (useNewValue) {
// The previous node value has moved to a leaf
// The node containing the new leaf should use the new value
node.value = valueHandler();
this._onItemAdded();
}
else {
// Clear the value as it was copied in the branch
node.value = null;
}
return newBranch;
}
_onItemAdded() {
this.length++;
this.emit('add', this.length);
}
}
function iteratorToArray(value, iterator) {
const values = [ value ];
let item = iterator.next();
while (!item.done) {
values.push(item.value);
item = iterator.next();
}
return values;
}
module.exports = Tree;