diff --git a/lib/aerospike.js b/lib/aerospike.js index 0e2414097..b5f0b230b 100644 --- a/lib/aerospike.js +++ b/lib/aerospike.js @@ -264,6 +264,14 @@ exports.Key = require('./key') */ exports.Record = require('./record') +/** + * In the Aerospike database, each record (similar to a row in a relational database) stores + * data using one or more bins (like columns in a relational database). + * + * @summary {@link Bin} class + */ +exports.Bin = require('./bin') + // ======================================================================== // Enumerations // ======================================================================== diff --git a/lib/bin.js b/lib/bin.js new file mode 100644 index 000000000..032a8b2b7 --- /dev/null +++ b/lib/bin.js @@ -0,0 +1,60 @@ +// ***************************************************************************** +// Copyright 2023 Aerospike, 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' + +/** + * @class Bin + * @classdesc Aerospike Bin + * + * In the Aerospike database, each record (similar to a row in a relational database) stores + * data using one or more bins (like columns in a relational database). The major difference + * between bins and RDBMS columns is that you don't need to define a schema. Each record can + * have multiple bins. Bins accept the data types listed {@link https://docs.aerospike.com/apidocs/nodejs/#toc4__anchor|here}. + * + * For information about these data types and how bins support them, see {@link https://docs.aerospike.com/server/guide/data-types/scalar-data-types|this}. + * + * Although the bin for a given record or object must be typed, bins in different rows do not + * have to be the same type. There are some internal performance optimizations for single-bin namespaces. + * + * @summary Construct a new Aerospike Bin instance. + * + */ +class Bin { + /** @private */ + constructor (name, value, mapOrder) { + /** + * Bin name. + * + * @member {String} Bin#name + */ + this.name = name + + /** + * Bin value. + * + * @member {Any} Bin#value + */ + if(mapOrder === 1){ + this.value = new Map(Object.entries(value)) + } + else{ + this.value = value + } + } +} + +module.exports = Bin \ No newline at end of file diff --git a/src/include/conversions.h b/src/include/conversions.h index 582eafa7d..ce44183a7 100644 --- a/src/include/conversions.h +++ b/src/include/conversions.h @@ -177,6 +177,8 @@ int list_from_jsarray(as_list **list, v8::Local array, const LogInfo *log); int map_from_jsobject(as_map **map, v8::Local obj, const LogInfo *log); +int map_from_jsmap(as_map **map, v8::Local obj, + const LogInfo *log); int asval_from_jsvalue(as_val **value, v8::Local v8value, const LogInfo *log); int string_from_jsarray(char*** roles, int roles_size, v8::Local role_array, const LogInfo *log); diff --git a/src/main/map_operations.cc b/src/main/map_operations.cc index eca3a24d3..ab244d5a9 100644 --- a/src/main/map_operations.cc +++ b/src/main/map_operations.cc @@ -157,7 +157,14 @@ bool add_map_put_items_op(as_operations *ops, const char *bin, as_v8_error(log, "Type error: items property should be an Object"); return false; } - if (map_from_jsobject(&items, v8items.As(), log) != + if(v8items->IsMap()){ + if (map_from_jsmap(&items, v8items.As(), log) != + AS_NODE_PARAM_OK) { + as_v8_error(log, "Type error: items property should be an Object"); + return false; + } + } + else if (map_from_jsobject(&items, v8items.As(), log) != AS_NODE_PARAM_OK) { as_v8_error(log, "Type error: items property should be an Object"); return false; diff --git a/src/main/util/conversions.cc b/src/main/util/conversions.cc index 0861ac92b..11e603f7b 100644 --- a/src/main/util/conversions.cc +++ b/src/main/util/conversions.cc @@ -37,8 +37,7 @@ extern "C" { #include #include #include -#include -#include +#include #include #include #include @@ -61,6 +60,7 @@ using namespace v8; const char *DoubleType = "Double"; const char *GeoJSONType = "GeoJSON"; +const char *BinType = "Bin"; const int64_t MIN_SAFE_INTEGER = -1 * (std::pow(2, 53) - 1); const int64_t MAX_SAFE_INTEGER = std::pow(2, 53) - 1; @@ -620,19 +620,20 @@ as_val *asval_clone(const as_val *val, const LogInfo *log) break; } case AS_MAP: { - as_hashmap *map = (as_hashmap *)as_map_fromval(val); + as_orderedmap *map = (as_orderedmap *)as_map_fromval(val); clone_val = - as_map_toval((as_map *)as_hashmap_new(as_hashmap_size(map))); - as_hashmap_iterator it; - as_hashmap_iterator_init(&it, map); - while (as_hashmap_iterator_has_next(&it)) { - as_pair *pair = (as_pair *)as_hashmap_iterator_next(&it); + as_map_toval((as_map *)as_orderedmap_new(as_orderedmap_size(map))); + as_orderedmap_iterator it; + as_orderedmap_iterator_init(&it, map); + while (as_orderedmap_iterator_has_next(&it)) { + as_pair *pair = (as_pair *)as_orderedmap_iterator_next(&it); as_val *orig_key = as_pair_1(pair); as_val *orig_val = as_pair_2(pair); as_val *clone_key = asval_clone(orig_key, log); as_val *clone_mapval = asval_clone(orig_val, log); - as_hashmap_set((as_hashmap *)clone_val, clone_key, clone_mapval); + as_orderedmap_set((as_orderedmap *)clone_val, clone_key, clone_mapval); } + as_orderedmap_set_flags((as_orderedmap *)clone_val, (as_orderedmap *)val->_.flags); as_v8_detail(log, "Cloning a map SUCCESS"); break; } @@ -860,12 +861,12 @@ Local val_to_jsvalue(as_val *val, const LogInfo *log) } case AS_MAP: { Local jsobj = Nan::New(); - as_hashmap *map = (as_hashmap *)as_map_fromval(val); - as_hashmap_iterator it; - as_hashmap_iterator_init(&it, map); + as_orderedmap *map = (as_orderedmap *)as_map_fromval(val); + as_orderedmap_iterator it; + as_orderedmap_iterator_init(&it, map); - while (as_hashmap_iterator_has_next(&it)) { - as_pair *p = (as_pair *)as_hashmap_iterator_next(&it); + while (as_orderedmap_iterator_has_next(&it)) { + as_pair *p = (as_pair *)as_orderedmap_iterator_next(&it); as_val *key = as_pair_1(p); as_val *val = as_pair_2(p); Nan::Set(jsobj, val_to_jsvalue(key, log), val_to_jsvalue(val, log)); @@ -1026,6 +1027,11 @@ bool is_geojson_value(Local value) return instanceof (value, GeoJSONType); } +bool is_bin_value(Local value) +{ + return instanceof (value, BinType); +} + char *geojson_as_string(Local value) { Local strval = @@ -1081,6 +1087,31 @@ int map_from_jsobject(as_map **map, Local obj, const LogInfo *log) return AS_NODE_PARAM_OK; } +int map_from_jsmap(as_map **map, Local obj, const LogInfo *log) +{ + const Local data = obj->AsArray(); + const uint32_t capacity = data->Length(); + as_v8_detail(log, "Creating new as_orderedmap with capacity %d", capacity); + as_orderedmap *orderedmap = as_orderedmap_new(capacity); + if (orderedmap == NULL) { + as_v8_error(log, "Map allocation failed"); + Nan::ThrowError("Map allocation failed"); + return AS_NODE_PARAM_ERR; + } + *map = (as_map *)orderedmap; + + for (uint32_t i = 0; i < capacity; i = i + 2) { + const Local name = Nan::Get(data, i).ToLocalChecked(); + const Local value = Nan::Get(data, i+1).ToLocalChecked(); + as_val *val = NULL; + if (asval_from_jsvalue(&val, value, log) != AS_NODE_PARAM_OK) { + return AS_NODE_PARAM_ERR; + } + as_stringmap_set(*map, *Nan::Utf8String(name), val); + } + return AS_NODE_PARAM_OK; +} + int asval_from_jsvalue(as_val **value, Local v8value, const LogInfo *log) { if (v8value->IsNull()) { @@ -1145,6 +1176,12 @@ int asval_from_jsvalue(as_val **value, Local v8value, const LogInfo *log) return AS_NODE_PARAM_ERR; } } + else if (v8value->IsMap()) { + if (map_from_jsmap((as_map **)value, v8value.As(), log) != + AS_NODE_PARAM_OK) { + return AS_NODE_PARAM_ERR; + } + } else if (is_geojson_value(v8value)) { char *jsonstr = geojson_as_string(v8value); *value = (as_val *)as_geojson_new(jsonstr, true); @@ -1242,6 +1279,11 @@ int privileges_from_jsarray(as_privilege*** privileges, int privileges_size, Loc int recordbins_from_jsobject(as_record *rec, Local obj, const LogInfo *log) { + if (is_bin_value(obj)) { + Local jsobj = Nan::New(); + Nan::Set(jsobj, Nan::Get(obj, Nan::New("name").ToLocalChecked()).ToLocalChecked(), Nan::Get(obj, Nan::New("value").ToLocalChecked()).ToLocalChecked()); + obj = jsobj; + } const Local props = Nan::GetOwnPropertyNames(obj).ToLocalChecked(); const uint32_t count = props->Length(); as_record_init(rec, count); @@ -1319,6 +1361,15 @@ int recordbins_from_jsobject(as_record *rec, Local obj, as_record_set_list(rec, *n, list); continue; } + if (value->IsMap()) { + as_map *map; + if (map_from_jsmap(&map, value.As(), log) != + AS_NODE_PARAM_OK) { + return AS_NODE_PARAM_ERR; + } + as_record_set_map(rec, *n, map); + continue; + } if (value->IsObject()) { as_map *map; if (map_from_jsobject(&map, value.As(), log) != diff --git a/test/maps.js b/test/maps.js index 40f613a73..aae92cb73 100644 --- a/test/maps.js +++ b/test/maps.js @@ -253,6 +253,16 @@ describe('client.operate() - CDT Map operations', function () { .then(cleanup()) }) + it('adds each item from the Map class to the map and returns the size of the map', function () { + console.log(maps.putItems('map', new Map([ ['e', 150,], ['d', 100], ['c', 99]]))) + return initState() + .then(createRecord({ map: { a: 1, b: 2, c: 3 } })) + .then(operate(maps.putItems('map', new Map([ ['e', 150], ['d', 100], ['c', 99]])))) + .then(assertResultEql({ map: 4 })) + .then(assertRecordEql({ map: { a: 1, b: 2, c: 99, d: 100 } })) + .then(cleanup()) + }) + context('with update-only flag', function () { helper.skipUnlessVersion('>= 4.3.0', this) diff --git a/test/put.js b/test/put.js index 01ec814dd..28536b565 100644 --- a/test/put.js +++ b/test/put.js @@ -240,11 +240,12 @@ describe('client.put()', function () { putGetVerify(record, expected, done) }) - it.skip('writes bin with Map value as map and reads it back', function (done) { + it('writes bin with Map value as map and reads it back as an ordered object', function (done) { const record = { - map: new Map([['a', 1], ['b', 'foo'], ['c', 1.23], + map: new Map([['g', [1, 2, 3]], ['h', { a: 1, b: 2 }], ['j', new Map([['b', 'foo'], ['a', 1]])], ['d', new Double(3.14)], ['e', Buffer.from('bar')], ['f', GeoJSON.Point(103.8, 1.283)], - ['g', [1, 2, 3]], ['h', { a: 1, b: 2 }]]) + ['a', 1], ['b', 'foo'], ['c', 1.23]] + ) } const expected = { map: { @@ -255,7 +256,36 @@ describe('client.put()', function () { e: Buffer.from('bar'), f: '{"type":"Point","coordinates":[103.8,1.283]}', g: [1, 2, 3], - h: { a: 1, b: 2 } + h: { a: 1, b: 2 }, + j: { a: 1, b: 'foo'} + } + } + putGetVerify(record, expected, done) + }) + + it('writes bin with the Bin class and reads it back as an object', function (done) { + const record = new Aerospike.Bin('map', { + g: [1, 2, 3], + h: { a: 1, b: 2 }, + j: new Map([['b', 'foo'], ['a', 1]]), + e: Buffer.from('bar'), + f: '{"type":"Point","coordinates":[103.8,1.283]}', + a: 1, + b: 'foo', + c: 1.23, + d: 3.14 + }) + const expected = { + map: { + a: 1, + b: 'foo', + c: 1.23, + d: 3.14, + e: Buffer.from('bar'), + f: '{"type":"Point","coordinates":[103.8,1.283]}', + g: [1, 2, 3], + h: { a: 1, b: 2 }, + j: { a: 1, b: 'foo'} } } putGetVerify(record, expected, done)