Skip to content

Commit c9da35b

Browse files
authored
fix(excludefromindexes): update logic to add all properties of Array embedded entities (#182)
The v4.2.0 of the Datastore client allows wildcard "*" to target all the properties of an embedded entity. The logic to define the Array of excludeFromIndexes has been updated to make use of it. fix #132
1 parent cc11e02 commit c9da35b

File tree

6 files changed

+141
-159
lines changed

6 files changed

+141
-159
lines changed

lib/entity.js

Lines changed: 86 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class Entity {
1616
constructor(data, id, ancestors, namespace, key) {
1717
this.className = 'Entity';
1818
this.schema = this.constructor.schema;
19-
this.excludeFromIndexes = [];
19+
this.excludeFromIndexes = {};
20+
2021
/**
2122
* Object to store custom data for the entity.
2223
* In some cases we might want to add custom data onto the entity
@@ -36,8 +37,8 @@ class Entity {
3637

3738
this.setId();
3839

39-
// create entityData from data passed
40-
this.entityData = buildEntityData(this, data || {});
40+
// create entityData from data provided
41+
this.____buildEntityData(data || {});
4142

4243
/**
4344
* Create virtual properties (getters and setters for entityData object)
@@ -333,6 +334,88 @@ class Entity {
333334

334335
return entityData;
335336
}
337+
338+
____buildEntityData(data) {
339+
const { schema } = this;
340+
const isJoiSchema = schema.isJoi;
341+
342+
// If Joi schema, get its default values
343+
if (isJoiSchema) {
344+
const { error, value } = schema.validateJoi(data);
345+
346+
if (!error) {
347+
this.entityData = { ...value };
348+
}
349+
}
350+
351+
this.entityData = { ...this.entityData, ...data };
352+
353+
let isArray;
354+
let isObject;
355+
356+
Object.entries(schema.paths).forEach(([key, prop]) => {
357+
const hasValue = {}.hasOwnProperty.call(this.entityData, key);
358+
const isOptional = {}.hasOwnProperty.call(prop, 'optional') && prop.optional !== false;
359+
const isRequired = {}.hasOwnProperty.call(prop, 'required') && prop.required === true;
360+
361+
// Set Default Values
362+
if (!isJoiSchema && !hasValue && !isOptional) {
363+
let value = null;
364+
365+
if ({}.hasOwnProperty.call(prop, 'default')) {
366+
if (typeof prop.default === 'function') {
367+
value = prop.default();
368+
} else {
369+
value = prop.default;
370+
}
371+
}
372+
373+
if (({}).hasOwnProperty.call(defaultValues.__map__, value)) {
374+
/**
375+
* If default value is in the gstore.defaultValue hashTable
376+
* then execute the handler for that shortcut
377+
*/
378+
value = defaultValues.__handler__(value);
379+
} else if (value === null && {}.hasOwnProperty.call(prop, 'values') && !isRequired) {
380+
// Default to first value of the allowed values if **not** required
381+
[value] = prop.values;
382+
}
383+
384+
this.entityData[key] = value;
385+
}
386+
387+
// Set excludeFromIndexes
388+
// ----------------------
389+
isArray = prop.type === Array || (prop.joi && prop.joi._type === 'array');
390+
isObject = prop.type === Object || (prop.joi && prop.joi._type === 'object');
391+
392+
if (prop.excludeFromIndexes === true) {
393+
if (isArray) {
394+
// We exclude both the array values + all the child properties of object items
395+
this.excludeFromIndexes[key] = [`${key}[]`, `${key}[].*`];
396+
} else if (isObject) {
397+
// We exclude the emmbeded entity + all its properties
398+
this.excludeFromIndexes[key] = [key, `${key}.*`];
399+
} else {
400+
this.excludeFromIndexes[key] = [key];
401+
}
402+
} else if (prop.excludeFromIndexes !== false) {
403+
const excludedArray = arrify(prop.excludeFromIndexes);
404+
if (isArray) {
405+
// The format to exclude a property from an embedded entity inside
406+
// an array is: "myArrayProp[].embeddedKey"
407+
this.excludeFromIndexes[key] = excludedArray.map(propExcluded => `${key}[].${propExcluded}`);
408+
} else if (isObject) {
409+
// The format to exclude a property from an embedded entity
410+
// is: "myEmbeddedEntity.key"
411+
this.excludeFromIndexes[key] = excludedArray.map(propExcluded => `${key}.${propExcluded}`);
412+
}
413+
}
414+
});
415+
416+
// add Symbol Key to the entityData
417+
this.entityData[this.gstore.ds.KEY] = this.entityKey;
418+
}
336419
}
337420

338421
// Private
@@ -367,90 +450,6 @@ function createKey(self, id, ancestors, namespace) {
367450
return namespace ? self.gstore.ds.key({ namespace, path }) : self.gstore.ds.key(path);
368451
}
369452

370-
function buildEntityData(self, data) {
371-
const { schema } = self;
372-
const isJoiSchema = schema.isJoi;
373-
374-
let entityData;
375-
376-
// If Joi schema, get its default values
377-
if (isJoiSchema) {
378-
const { error, value } = schema.validateJoi(data);
379-
380-
if (!error) {
381-
entityData = { ...value };
382-
}
383-
}
384-
385-
entityData = { ...entityData, ...data };
386-
387-
let isTypeArray;
388-
389-
Object.keys(schema.paths).forEach(k => {
390-
const prop = schema.paths[k];
391-
const hasValue = {}.hasOwnProperty.call(entityData, k);
392-
const isOptional = {}.hasOwnProperty.call(prop, 'optional') && prop.optional !== false;
393-
const isRequired = {}.hasOwnProperty.call(prop, 'required') && prop.required === true;
394-
395-
// Set Default Values
396-
if (!isJoiSchema && !hasValue && !isOptional) {
397-
let value = null;
398-
399-
if ({}.hasOwnProperty.call(prop, 'default')) {
400-
if (typeof prop.default === 'function') {
401-
value = prop.default();
402-
} else {
403-
value = prop.default;
404-
}
405-
}
406-
407-
if (({}).hasOwnProperty.call(defaultValues.__map__, value)) {
408-
/**
409-
* If default value is in the gstore.defaultValue hashTable
410-
* then execute the handler for that shortcut
411-
*/
412-
value = defaultValues.__handler__(value);
413-
} else if (value === null && {}.hasOwnProperty.call(prop, 'values') && !isRequired) {
414-
// Default to first value of the allowed values if **not** required
415-
[value] = prop.values;
416-
}
417-
418-
entityData[k] = value;
419-
}
420-
421-
// Set excludeFromIndexes
422-
// ----------------------
423-
isTypeArray = prop.type === 'array' || (prop.joi && prop.joi._type === 'array');
424-
425-
if (prop.excludeFromIndexes === true && !isTypeArray) {
426-
self.excludeFromIndexes.push(k);
427-
} else if (!is.boolean(prop.excludeFromIndexes)) {
428-
// For embedded entities we can set which properties are excluded from indexes
429-
// by passing a string|array of properties
430-
431-
let formatted;
432-
const exFromIndexes = arrify(prop.excludeFromIndexes);
433-
434-
if (prop.type === 'array') {
435-
// The format to exclude a property from an embedded entity inside
436-
// an array is: "myArrayProp[].embeddedKey"
437-
formatted = exFromIndexes.map(excluded => `${k}[].${excluded}`);
438-
} else {
439-
// The format to exclude a property from an embedded entity
440-
// is: "myEmbeddedEntity.key"
441-
formatted = exFromIndexes.map(excluded => `${k}.${excluded}`);
442-
}
443-
444-
self.excludeFromIndexes = [...self.excludeFromIndexes, ...formatted];
445-
}
446-
});
447-
448-
// add Symbol Key to the entityData
449-
entityData[self.gstore.ds.KEY] = self.entityKey;
450-
451-
return entityData;
452-
}
453-
454453
function registerHooksFromSchema(self) {
455454
const callQueue = self.schema.callQueue.entity;
456455

lib/serializers/datastore.js

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,41 +36,11 @@ function toDatastore(entity, options = {}) {
3636
// ---------
3737

3838
function getExcludeFromIndexes() {
39-
const excluded = [...entity.excludeFromIndexes] || [];
40-
let isArray;
41-
let isObject;
42-
let propConfig;
43-
let propValue;
44-
45-
Object.keys(data).forEach(prop => {
46-
propValue = entity.entityData[prop];
47-
if (propValue === null) {
48-
return;
49-
}
50-
propConfig = entity.schema.paths[prop];
51-
52-
isArray = propConfig && (propConfig.type === 'array'
53-
|| (propConfig.joi && propConfig.joi._type === 'array'));
54-
55-
isObject = propConfig && (propConfig.type === 'object'
56-
|| (propConfig.joi && propConfig.joi._type === 'object'));
57-
58-
if (isArray && propConfig.excludeFromIndexes === true) {
59-
// We exclude all the primitives from Array
60-
// The format is "entityProp[]"
61-
excluded.push(`${prop}[]`);
62-
} else if (isObject && propConfig.excludeFromIndexes === true) {
63-
// For "object" type we automatically set all its properties to excludeFromIndexes: true
64-
// which is what most of us expect.
65-
Object.keys(propValue).forEach(k => {
66-
// We add the embedded property to our Array of excludedFromIndexes
67-
// The format is "entityProp.entityKey"
68-
excluded.push(`${prop}.${k}`);
69-
});
70-
}
71-
});
72-
73-
return excluded;
39+
return Object.entries(data)
40+
.filter(({ 1: value }) => value !== null)
41+
.map(([key]) => entity.excludeFromIndexes[key])
42+
.filter(v => v !== undefined)
43+
.reduce((acc, arr) => [...acc, ...arr], []);
7444
}
7545
}
7646

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,6 @@
9999
"yargs": "^14.0.0"
100100
},
101101
"peerDependencies": {
102-
"@google-cloud/datastore": ">= 3.0.0 < 5"
102+
"@google-cloud/datastore": ">= 4.2.0 < 5"
103103
}
104104
}

test/entity-test.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ describe('Entity', () => {
7272
assert.isDefined(entity.schema);
7373
assert.isDefined(entity.pre);
7474
assert.isDefined(entity.post);
75-
expect(entity.excludeFromIndexes).deep.equal([]);
75+
expect(entity.excludeFromIndexes).deep.equal({});
7676
});
7777

7878
it('should add data passed to entityData', () => {
@@ -87,7 +87,7 @@ describe('Entity', () => {
8787

8888
it('should not add any data if nothing is passed', () => {
8989
schema = new Schema({
90-
name: { type: 'string', optional: true },
90+
name: { type: String, optional: true },
9191
});
9292
GstoreModel = gstore.model('BlogPost', schema);
9393

@@ -102,10 +102,10 @@ describe('Entity', () => {
102102
}
103103

104104
schema = new Schema({
105-
name: { type: 'string', default: 'John' },
106-
lastname: { type: 'string' },
105+
name: { type: String, default: 'John' },
106+
lastname: { type: String },
107107
email: { optional: true },
108-
generatedValue: { type: 'string', default: fn },
108+
generatedValue: { type: String, default: fn },
109109
availableValues: { values: ['a', 'b', 'c'] },
110110
availableValuesRequired: { values: ['a', 'b', 'c'], required: true },
111111
});
@@ -159,7 +159,7 @@ describe('Entity', () => {
159159
it('should call handler for default values in gstore.defaultValues constants', () => {
160160
sinon.spy(gstore.defaultValues, '__handler__');
161161
schema = new Schema({
162-
createdOn: { type: 'dateTime', default: gstore.defaultValues.NOW },
162+
createdOn: { type: Date, default: gstore.defaultValues.NOW },
163163
});
164164
GstoreModel = gstore.model('BlogPost', schema);
165165
entity = new GstoreModel({});
@@ -170,7 +170,7 @@ describe('Entity', () => {
170170

171171
it('should not add default to optional properties', () => {
172172
schema = new Schema({
173-
name: { type: 'string' },
173+
name: { type: String },
174174
email: { optional: true },
175175
});
176176
GstoreModel = gstore.model('BlogPost', schema);
@@ -183,20 +183,27 @@ describe('Entity', () => {
183183
it('should create its array of excludeFromIndexes', () => {
184184
schema = new Schema({
185185
name: { excludeFromIndexes: true },
186-
age: { excludeFromIndexes: true, type: 'int' },
187-
embedded: { excludeFromIndexes: ['prop1', 'prop2'] },
188-
arrayValue: { excludeFromIndexes: 'property', type: 'array' },
186+
age: { excludeFromIndexes: true, type: Number },
187+
embedded: { type: Object, excludeFromIndexes: ['prop1', 'prop2'] },
188+
embedded2: { type: Object, excludeFromIndexes: true },
189+
arrayValue: { excludeFromIndexes: 'property', type: Array },
189190
// Array in @google-cloud have to be set on the data value
190-
arrayValue2: { excludeFromIndexes: true, type: 'array' },
191+
arrayValue2: { excludeFromIndexes: true, type: Array },
191192
arrayValue3: { excludeFromIndexes: true, joi: Joi.array() },
192193
});
193194
GstoreModel = gstore.model('BlogPost', schema);
194195

195196
entity = new GstoreModel({ name: 'John' });
196197

197-
expect(entity.excludeFromIndexes).deep.equal([
198-
'name', 'age', 'embedded.prop1', 'embedded.prop2', 'arrayValue[].property',
199-
]);
198+
expect(entity.excludeFromIndexes).deep.equal({
199+
name: ['name'],
200+
age: ['age'],
201+
embedded: ['embedded.prop1', 'embedded.prop2'],
202+
embedded2: ['embedded2', 'embedded2.*'],
203+
arrayValue: ['arrayValue[].property'],
204+
arrayValue2: ['arrayValue2[]', 'arrayValue2[].*'],
205+
arrayValue3: ['arrayValue3[]', 'arrayValue3[].*'],
206+
});
200207
});
201208

202209
describe('should create Datastore Key', () => {
@@ -1102,7 +1109,7 @@ describe('Entity', () => {
11021109
});
11031110

11041111
it('should update modifiedOn to new Date if property in Schema', () => {
1105-
schema = new Schema({ modifiedOn: { type: 'datetime' } });
1112+
schema = new Schema({ modifiedOn: { type: Date } });
11061113
GstoreModel = gstore.model('BlogPost', schema);
11071114
entity = new GstoreModel({});
11081115

@@ -1114,7 +1121,7 @@ describe('Entity', () => {
11141121
});
11151122

11161123
it('should convert plain geo object (latitude, longitude) to datastore GeoPoint', () => {
1117-
schema = new Schema({ location: { type: 'geoPoint' } });
1124+
schema = new Schema({ location: { type: Schema.Types.GeoPoint } });
11181125
GstoreModel = gstore.model('Car', schema);
11191126
entity = new GstoreModel({
11201127
location: {

test/model-test.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,7 +1334,12 @@ describe('Model', () => {
13341334

13351335
const entity = new GstoreModel({});
13361336

1337-
expect(entity.excludeFromIndexes).deep.equal(['lastname', 'age'].concat(arr));
1337+
expect(entity.excludeFromIndexes).deep.equal({
1338+
lastname: ['lastname'],
1339+
age: ['age'],
1340+
newProp: ['newProp'],
1341+
url: ['url'],
1342+
});
13381343
expect(schema.path('newProp').optional).equal(true);
13391344
});
13401345

@@ -1344,7 +1349,10 @@ describe('Model', () => {
13441349

13451350
const entity = new GstoreModel({});
13461351

1347-
expect(entity.excludeFromIndexes).deep.equal(['lastname', 'age']);
1352+
expect(entity.excludeFromIndexes).deep.equal({
1353+
lastname: ['lastname'],
1354+
age: ['age'],
1355+
});
13481356
assert.isUndefined(schema.path('lastname').optional);
13491357
expect(schema.path('lastname').excludeFromIndexes).equal(true);
13501358
});

0 commit comments

Comments
 (0)