Skip to content

Commit 3edf1af

Browse files
authored
Support transforming column values in the Mapper
1 parent ceb29dc commit 3edf1af

File tree

15 files changed

+647
-72
lines changed

15 files changed

+647
-72
lines changed

doc/features/mapper/defining-mappings/README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ To use multiple tables/views with the same model, specify the names in the `Mapp
6464
```javascript
6565
const mappingOptions = {
6666
models: {
67-
'User': {
67+
'Video': {
6868
tables: [ 'videos', 'user_videos', 'latest_videos' ],
6969
mappings: new UnderscoreCqlToCamelCaseMappings(),
7070
columns: {
@@ -90,6 +90,38 @@ When selecting rows, the most suitable table will be used according to the table
9090
const result = await videoMapper.find({ userId });
9191
```
9292

93+
## Using custom type conversions
94+
95+
When representing your model properties in a way that requires transformation from and to the column values, you can
96+
define `fromModel` and `toModel` functions to perform the conversions.
97+
98+
For example, consider a `text` column that contains a JSON string and you want to represent it as an JavaScript
99+
`Object`.
100+
101+
```javascript
102+
const mappingOptions = {
103+
models: {
104+
'User': {
105+
tables: ['users'],
106+
mappings: new UnderscoreCqlToCamelCaseMappings(),
107+
columns: {
108+
'userid': 'userId',
109+
'info': {
110+
name: 'user_info',
111+
fromModel: JSON.stringify,
112+
toModel: JSON.parse
113+
}
114+
}
115+
}
116+
}
117+
};
118+
```
119+
120+
Now, you can interact with the model property as an `Object` rather than as a `String`.
121+
122+
```javascript
123+
await userMapper.insert({ userId, info: { birthdate, favoriteBrowser } });
124+
```
93125

94126
## Mapping to a Materialized View
95127

doc/features/mapper/getting-started/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const Mapper = cassandra.mapping.Mapper;
1010
const client = new Client({ contactPoints, localDataCenter, keyspace });
1111
```
1212

13-
Create a `Mapper` instance and reuse it across your application. You can specify you model properties and how those
14-
are mapped to table columns can be defined in the [MappingOptions](../defining-mappings/).
13+
Create a `Mapper` instance and reuse it across your application. You can define how your model properties are mapped
14+
to table columns in the [MappingOptions](../defining-mappings/).
1515

1616
```javascript
1717
const mapper = new Mapper(client, {

lib/mapping/doc-info-adapter.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ class DocInfoAdapter {
4141
}
4242

4343
return propertyKeys.map(propertyName => ({
44-
propertyName, columnName: mappingInfo.getColumnName(propertyName), value: doc[propertyName]
44+
propertyName,
45+
columnName: mappingInfo.getColumnName(propertyName),
46+
value: doc[propertyName],
47+
fromModel: mappingInfo.getFromModelFn(propertyName)
4548
}));
4649
}
4750

lib/mapping/index.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,16 @@ export namespace mapping {
112112
type ModelOptions = {
113113
tables?: string[] | ModelTables[];
114114
mappings?: TableMappings;
115-
columns?: { [key: string]: string };
115+
columns?: { [key: string]: string|ModelColumnOptions };
116116
keyspace?: string;
117117
}
118118

119+
type ModelColumnOptions = {
120+
name: string;
121+
toModel?: (columnValue: any) => any;
122+
fromModel?: (modelValue: any) => any;
123+
};
124+
119125
interface ModelBatchItem {
120126

121127
}

lib/mapping/mapping-handler.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class MappingHandler {
134134
_executeSelect(query, paramsGetter, doc, docInfo, executionOptions, cacheItem) {
135135
const options = DocInfoAdapter.adaptAllOptions(executionOptions, true);
136136

137-
return this._client.execute(query, paramsGetter(doc, docInfo), options)
137+
return this._client.execute(query, paramsGetter(doc, docInfo, this.info), options)
138138
.then(rs => {
139139
if (cacheItem.resultAdapter === null) {
140140
cacheItem.resultAdapter = ResultMapper.getSelectAdapter(this.info, rs);
@@ -368,7 +368,7 @@ class MappingHandler {
368368
cacheItem.executor = function singleExecutor(doc, docInfo, executionOptions) {
369369
const options = DocInfoAdapter.adaptOptions(executionOptions, queryInfo.isIdempotent);
370370

371-
return self._client.execute(queryInfo.query, queryInfo.paramsGetter(doc, docInfo), options)
371+
return self._client.execute(queryInfo.query, queryInfo.paramsGetter(doc, docInfo, self.info), options)
372372
.then(rs => new Result(rs, self.info, ResultMapper.getMutationAdapter(rs)));
373373
};
374374

@@ -385,7 +385,7 @@ class MappingHandler {
385385
// Use the params getter function to obtain the parameters each time
386386
const queryAndParams = queries.map(q => ({
387387
query: q.query,
388-
params: q.paramsGetter(doc, docInfo)
388+
params: q.paramsGetter(doc, docInfo, self.info)
389389
}));
390390

391391
const options = DocInfoAdapter.adaptOptions(executionOptions, isIdempotent);

lib/mapping/model-mapping-info.js

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,37 +29,52 @@ class ModelMappingInfo {
2929
* @param {String} keyspace
3030
* @param {Array<{name, isView}>} tables
3131
* @param {TableMappings} mappings
32-
* @param {Map<String,String>} columns
32+
* @param {Map<String,ModelColumnInfo>} columns
3333
*/
3434
constructor(keyspace, tables, mappings, columns) {
3535
this.keyspace = keyspace;
3636
this.tables = tables;
3737
this._mappings = mappings;
3838
this._columns = columns;
39+
40+
// Define a map of column information per property name
41+
/** @type {Map<String, ModelColumnInfo>} */
3942
this._documentProperties = new Map();
40-
columns.forEach((propName, columnName) => this._documentProperties.set(propName, columnName));
43+
for (const modelColumnInfo of columns.values()) {
44+
this._documentProperties.set(modelColumnInfo.propertyName, modelColumnInfo);
45+
}
4146
}
4247

4348
getColumnName(propName) {
44-
const columnName = this._documentProperties.get(propName);
45-
if (columnName !== undefined) {
49+
const modelColumnInfo = this._documentProperties.get(propName);
50+
if (modelColumnInfo !== undefined) {
4651
// There is an specific name transformation between the column name and the property name
47-
return columnName;
52+
return modelColumnInfo.columnName;
4853
}
4954
// Rely on the TableMappings (i.e. maybe there is a convention defined for this property)
5055
return this._mappings.getColumnName(propName);
5156
}
5257

5358
getPropertyName(columnName) {
54-
const propName = this._columns.get(columnName);
55-
if (propName !== undefined) {
59+
const modelColumnInfo = this._columns.get(columnName);
60+
if (modelColumnInfo !== undefined) {
5661
// There is an specific name transformation between the column name and the property name
57-
return propName;
62+
return modelColumnInfo.propertyName;
5863
}
5964
// Rely on the TableMappings (i.e. maybe there is a convention defined for this column)
6065
return this._mappings.getPropertyName(columnName);
6166
}
6267

68+
getFromModelFn(propName) {
69+
const modelColumnInfo = this._documentProperties.get(propName);
70+
return modelColumnInfo !== undefined ? modelColumnInfo.fromModel : null;
71+
}
72+
73+
getToModelFn(columnName) {
74+
const modelColumnInfo = this._columns.get(columnName);
75+
return modelColumnInfo !== undefined ? modelColumnInfo.toModel : null;
76+
}
77+
6378
newInstance() {
6479
return this._mappings.newObjectInstance();
6580
}
@@ -123,7 +138,7 @@ class ModelMappingInfo {
123138
const columns = new Map();
124139
if (modelOptions.columns !== null && typeof modelOptions.columns === 'object') {
125140
Object.keys(modelOptions.columns).forEach(columnName => {
126-
columns.set(columnName, modelOptions.columns[columnName]);
141+
columns.set(columnName, ModelColumnInfo.parse(columnName, modelOptions.columns[columnName]));
127142
});
128143
}
129144

@@ -144,4 +159,36 @@ class ModelMappingInfo {
144159
}
145160
}
146161

162+
class ModelColumnInfo {
163+
constructor(columnName, propertyName, toModel, fromModel) {
164+
this.columnName = columnName;
165+
this.propertyName = propertyName;
166+
167+
if (toModel && typeof toModel !== 'function') {
168+
throw new TypeError(`toModel type for property '${propertyName}' should be a function (obtained ${
169+
typeof toModel})`);
170+
}
171+
172+
if (fromModel && typeof fromModel !== 'function') {
173+
throw new TypeError(`fromModel type for property '${propertyName}' should be a function (obtained ${
174+
typeof fromModel})`);
175+
}
176+
177+
this.toModel = toModel;
178+
this.fromModel = fromModel;
179+
}
180+
181+
static parse(columnName, value) {
182+
if (!value) {
183+
return new ModelColumnInfo(columnName, columnName);
184+
}
185+
186+
if (typeof value === 'string') {
187+
return new ModelColumnInfo(columnName, value);
188+
}
189+
190+
return new ModelColumnInfo(columnName, value.name || columnName, value.toModel, value.fromModel);
191+
}
192+
}
193+
147194
module.exports = ModelMappingInfo;

lib/mapping/q.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ class QueryOperator {
2828
* @param {String} key
2929
* @param value
3030
* @param [hasChildValues]
31+
* @param [isInOperator]
3132
*/
32-
constructor(key, value, hasChildValues) {
33+
constructor(key, value, hasChildValues, isInOperator) {
3334
/**
3435
* The CQL key representing the operator
3536
* @type {string}
@@ -45,6 +46,11 @@ class QueryOperator {
4546
* Determines whether a query operator can have child values or operators (AND, OR)
4647
*/
4748
this.hasChildValues = hasChildValues;
49+
50+
/**
51+
* Determines whether this instance represents CQL "IN" operator.
52+
*/
53+
this.isInOperator = isInOperator;
4854
}
4955
}
5056

@@ -95,7 +101,7 @@ const q = {
95101
if (!Array.isArray(arr)) {
96102
throw new errors.ArgumentError('IN operator supports only Array values');
97103
}
98-
return new QueryOperator('IN', arr);
104+
return new QueryOperator('IN', arr, false, true);
99105
},
100106

101107
gt: function gt(value) {

lib/mapping/query-generator.js

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class QueryGenerator {
6363
}
6464

6565
static selectParamsGetter(propertiesInfo, limit) {
66-
let scriptText = '(function getParametersSelect(doc, docInfo) {\n';
66+
let scriptText = '(function getParametersSelect(doc, docInfo, mappingInfo) {\n';
6767
scriptText += ' return [';
6868

6969
scriptText += QueryGenerator._valueGetterExpression(propertiesInfo);
@@ -78,8 +78,8 @@ class QueryGenerator {
7878
// Finish return statement
7979
scriptText += '];\n})';
8080

81-
const script = new vm.Script(scriptText);
82-
return script.runInThisContext({ filename: vmFileName});
81+
const script = new vm.Script(scriptText, { filename: vmFileName });
82+
return script.runInThisContext();
8383
}
8484

8585
/**
@@ -132,7 +132,7 @@ class QueryGenerator {
132132
}
133133

134134
static _insertParamsGetter(propertiesInfo, docInfo) {
135-
let scriptText = '(function getParametersInsert(doc, docInfo) {\n';
135+
let scriptText = '(function getParametersInsert(doc, docInfo, mappingInfo) {\n';
136136
scriptText += ' return [';
137137

138138
scriptText += QueryGenerator._valueGetterExpression(propertiesInfo);
@@ -144,8 +144,8 @@ class QueryGenerator {
144144
// Finish return statement
145145
scriptText += '];\n})';
146146

147-
const script = new vm.Script(scriptText);
148-
return script.runInThisContext({ filename: vmFileName});
147+
const script = new vm.Script(scriptText, { filename: vmFileName });
148+
return script.runInThisContext();
149149
}
150150

151151
/**
@@ -249,7 +249,7 @@ class QueryGenerator {
249249
* @returns {Function}
250250
*/
251251
static _updateParamsGetter(primaryKeys, propertiesInfo, when, ttl) {
252-
let scriptText = '(function getParametersUpdate(doc, docInfo) {\n';
252+
let scriptText = '(function getParametersUpdate(doc, docInfo, mappingInfo) {\n';
253253
scriptText += ' return [';
254254

255255
if (typeof ttl === 'number') {
@@ -271,8 +271,8 @@ class QueryGenerator {
271271
// Finish return statement
272272
scriptText += '];\n})';
273273

274-
const script = new vm.Script(scriptText);
275-
return script.runInThisContext({ filename: vmFileName});
274+
const script = new vm.Script(scriptText, { filename: vmFileName });
275+
return script.runInThisContext();
276276
}
277277

278278
/**
@@ -346,7 +346,7 @@ class QueryGenerator {
346346
* @returns {Function}
347347
*/
348348
static _deleteParamsGetter(primaryKeys, propertiesInfo, when) {
349-
let scriptText = '(function getParametersDelete(doc, docInfo) {\n';
349+
let scriptText = '(function getParametersDelete(doc, docInfo, mappingInfo) {\n';
350350
scriptText += ' return [';
351351

352352
// Where clause
@@ -360,8 +360,8 @@ class QueryGenerator {
360360
// Finish return statement
361361
scriptText += '];\n})';
362362

363-
const script = new vm.Script(scriptText);
364-
return script.runInThisContext({ filename: vmFileName});
363+
const script = new vm.Script(scriptText, { filename: vmFileName });
364+
return script.runInThisContext();
365365
}
366366

367367
/**
@@ -375,20 +375,29 @@ class QueryGenerator {
375375
objectName = objectName || 'doc';
376376

377377
return propertiesInfo
378-
.map(p => QueryGenerator._valueGetterSingle(`${objectName}['${p.propertyName}']`, p.value))
378+
.map(p =>
379+
QueryGenerator._valueGetterSingle(`${objectName}['${p.propertyName}']`, p.propertyName, p.value, p.fromModel))
379380
.join(', ');
380381
}
381382

382-
static _valueGetterSingle(prefix, value) {
383+
static _valueGetterSingle(prefix, propName, value, fromModelFn) {
384+
let valueGetter = prefix;
385+
383386
if (value instanceof QueryOperator) {
384387
if (value.hasChildValues) {
385-
return `${QueryGenerator._valueGetterSingle(`${prefix}.value[0]`, value.value[0])}` +
386-
`, ${QueryGenerator._valueGetterSingle(`${prefix}.value[1]`, value.value[1])}`;
388+
return `${QueryGenerator._valueGetterSingle(`${prefix}.value[0]`, propName, value.value[0], fromModelFn)}` +
389+
`, ${QueryGenerator._valueGetterSingle(`${prefix}.value[1]`, propName, value.value[1], fromModelFn)}`;
390+
}
391+
392+
valueGetter = `${prefix}.value`;
393+
394+
if (value.isInOperator && fromModelFn) {
395+
// Transform each individual value
396+
return `${valueGetter}.map(v => ${QueryGenerator._getMappingFunctionCall(propName, 'v')})`;
387397
}
388-
return `${prefix}.value`;
389398
}
390399

391-
return prefix;
400+
return !fromModelFn ? valueGetter : QueryGenerator._getMappingFunctionCall(propName, valueGetter);
392401
}
393402

394403
/**
@@ -402,7 +411,13 @@ class QueryGenerator {
402411
prefix = prefix || 'doc';
403412

404413
return propertiesInfo
405-
.map(p => `${prefix}['${p.propertyName}']${p.value instanceof QueryAssignment ? '.value' : ''}`)
414+
.map(p => {
415+
const valueGetter = `${prefix}['${p.propertyName}']${p.value instanceof QueryAssignment ? '.value' : ''}`;
416+
if (p.fromModel) {
417+
return QueryGenerator._getMappingFunctionCall(p.propertyName, valueGetter);
418+
}
419+
return valueGetter;
420+
})
406421
.join(', ');
407422
}
408423

@@ -412,6 +427,10 @@ class QueryGenerator {
412427
.join(' AND ');
413428
}
414429

430+
static _getMappingFunctionCall(propName, valueGetter) {
431+
return `mappingInfo.getFromModelFn('${propName}')(${valueGetter})`;
432+
}
433+
415434
static _getSingleCondition(columnName, value) {
416435
if (value instanceof QueryOperator) {
417436
if (value.hasChildValues) {

0 commit comments

Comments
 (0)