Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds new property namespace to index definitions #325

Merged
merged 4 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,4 +485,8 @@ All notable changes to this project will be documented in this file. Breaking ch

## [2.10.7] - 2023-11-09
### Fixed
- Fixes latency issue introduced in `2.10.4` affecting all queries discovered and brought forward by Ross Gerbasi. Thank you, Ross Gerbasi!
- Fixes latency issue introduced in `2.10.4` affecting all queries discovered and brought forward by Ross Gerbasi. Thank you, Ross Gerbasi!

## [2.11.0] - 2023-11-12
### Added
- Adds new property `scope` to index definitions, allowing users to further isolate partition keys beyond just `service` participation. This implements an RFC that was thoughtfully put forward by [@Sam3d](https://github.com/sam3d) in [Issue #290](https://github.com/tywalch/electrodb/issues/290). Thank you, Brooke for your contribution!
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3655,6 +3655,7 @@ export interface Schema<A extends string, F extends string, C extends string> {
[accessPattern: string]: {
readonly project?: "keys_only";
readonly index?: string;
readonly scope?: string;
readonly type?: "clustered" | "isolated";
readonly collection?: AccessPatternCollection<C>;
readonly pk: {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "electrodb",
"version": "2.10.7",
"version": "2.11.0",
"description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",
"main": "index.js",
"scripts": {
Expand Down
11 changes: 8 additions & 3 deletions src/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -3222,6 +3222,9 @@ class Entity {

// If keys are not custom, set the prefixes
if (!keys.pk.isCustom) {
if (tableIndex.scope) {
pk = `${pk}_${tableIndex.scope}`;
}
keys.pk.prefix = u.formatKeyCasing(pk, tableIndex.pk.casing);
}

Expand Down Expand Up @@ -3842,6 +3845,7 @@ class Entity {
let indexName = index.index || TableIndex;
let indexType =
typeof index.type === "string" ? index.type : IndexTypes.isolated;
let indexScope = index.scope || "";
if (indexType === "clustered") {
clusteredIndexes.add(accessPattern);
}
Expand Down Expand Up @@ -3940,14 +3944,15 @@ class Entity {
}
}

let definition = {
let definition= {
pk,
sk,
collection,
hasSk,
collection,
customFacets,
index: indexName,
type: indexType,
index: indexName,
scope: indexScope,
};

indexHasSubCollections[indexName] =
Expand Down
11 changes: 11 additions & 0 deletions src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,7 @@ class Service {
let pkFieldMatch = definition.pk.field === providedIndex.pk.field;
let pkFacetLengthMatch =
definition.pk.facets.length === providedIndex.pk.facets.length;
let scopeMatch = definition.scope === providedIndex.scope;
let mismatchedFacetLabels = [];
let collectionDifferences = [];
let definitionIndexName = u.formatIndexNameForDisplay(definition.index);
Expand Down Expand Up @@ -631,6 +632,16 @@ class Service {
}
}

if (!scopeMatch) {
collectionDifferences.push(
`The index scope value provided "${
providedIndex.scope || "undefined"
}" does not match established index scope value "${
definition.scope || "undefined"
}" on index "${providedIndexName}". Index scope options must match across all entities participating in a collection`,
);
}

if (!isCustomMatchPK) {
collectionDifferences.push(
`The usage of key templates the partition key on index ${definitionIndexName} must be consistent across all Entities, some entities provided use template while others do not`,
Expand Down
4 changes: 4 additions & 0 deletions src/validations.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ const Index = {
enum: ["string", "number"],
required: false,
},
scope: {
type: "string",
required: false,
}
},
},
sk: {
Expand Down
259 changes: 259 additions & 0 deletions test/ts_connected.entity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2573,4 +2573,263 @@ describe('field translation', () => {
});
});

describe('index scope', () => {
const serviceName = uuid();
const withScope = new Entity(
{
model: {
entity: serviceName,
service: 'test',
version: "1",
},
attributes: {
prop1: {
type: "string",
},
prop2: {
type: "string",
},
prop3: {
type: "string",
},
prop4: {
type: 'list',
items: {
type: 'string'
}
}
},
indexes: {
test: {
scope: 'scope1',
pk: {
field: "pk",
composite: ["prop1"],
},
sk: {
field: "sk",
composite: ["prop2"],
},
},
reverse: {
index: 'gsi1pk-gsi1sk-index',
scope: 'scope2',
pk: {
field: "gsi1pk",
composite: ["prop2"],
},
sk: {
field: "gsi1sk",
composite: ["prop1"],
},
},
},
},
{ table: "electro", client }
);

const withoutScope = new Entity(
{
model: {
entity: serviceName,
service: 'test',
version: "1",
},
attributes: {
prop1: {
type: "string",
},
prop2: {
type: "string",
},
prop3: {
type: "string",
},
prop4: {
type: 'list',
items: {
type: 'string'
}
}
},
indexes: {
test: {
pk: {
field: "pk",
composite: ["prop1"],
},
sk: {
field: "sk",
composite: ["prop2"],
},
},
reverse: {
index: 'gsi1pk-gsi1sk-index',
pk: {
field: "gsi1pk",
composite: ["prop2"],
},
sk: {
field: "gsi1sk",
composite: ["prop1"],
},
},
},
},
{ table: "electro", client }
);

it('should add scope value to all keys', () => {
const getParams = withScope.get({prop1: 'abc', prop2: 'def'}).params();
expect(getParams.Key.pk).to.equal('$test_scope1#prop1_abc');

const queryParams = withScope.query.test({prop1: 'abc'}).params();
expect(queryParams.ExpressionAttributeValues[':pk']).to.equal('$test_scope1#prop1_abc');

const queryParams2 = withScope.query.reverse({prop2: 'def'}).params();
expect(queryParams2.ExpressionAttributeValues[':pk']).to.equal('$test_scope2#prop2_def');

const scanParams = withScope.scan.params();
expect(scanParams.ExpressionAttributeValues[':pk']).to.equal('$test_scope1#prop1_');

const deleteParams = withScope.delete({prop1: 'abc', prop2: 'def'}).params();
expect(deleteParams.Key.pk).to.equal('$test_scope1#prop1_abc');

const removeParams = withScope.remove({prop1: 'abc', prop2: 'def'}).params();
expect(removeParams.Key.pk).to.equal('$test_scope1#prop1_abc');

const updateParams = withScope.update({prop1: 'abc', prop2: 'def'}).set({prop3: 'ghi'}).params();
expect(updateParams.Key.pk).to.equal('$test_scope1#prop1_abc');

const patchParams = withScope.patch({prop1: 'abc', prop2: 'def'}).set({prop3: 'ghi'}).params();
expect(patchParams.Key.pk).to.equal('$test_scope1#prop1_abc');

const putParams = withScope.put({prop1: 'abc', prop2: 'def'}).params();
expect(putParams.Item.pk).to.equal('$test_scope1#prop1_abc');

const createParams = withScope.create({prop1: 'abc', prop2: 'def'}).params();
expect(createParams.Item.pk).to.equal('$test_scope1#prop1_abc');

const upsertParams = withScope.upsert({prop1: 'abc', prop2: 'def'}).set({prop3: 'ghi'}).params();
expect(upsertParams.Key.pk).to.equal('$test_scope1#prop1_abc');

const batchGetParams = withScope.get([{prop1: 'abc', prop2: 'def'}]).params();
expect(batchGetParams[0].RequestItems.electro.Keys[0].pk).to.equal('$test_scope1#prop1_abc');

const batchDeleteParams = withScope.delete([{prop1: 'abc', prop2: 'def'}]).params();
expect(batchDeleteParams[0].RequestItems.electro[0].DeleteRequest.Key.pk).to.equal('$test_scope1#prop1_abc');

const batchPutParams = withScope.put([{prop1: 'abc', prop2: 'def'}]).params();
expect(batchPutParams[0].RequestItems.electro[0].PutRequest.Item.pk).to.equal('$test_scope1#prop1_abc');

const keys = withScope.conversions.fromComposite.toKeys({prop1: 'abc', prop2: 'def'});
expect(keys.pk).to.equal('$test_scope1#prop1_abc');
expect(keys.gsi1pk).to.equal('$test_scope2#prop2_def');

const keysComposite = withScope.conversions.fromKeys.toComposite(keys);
expect(keysComposite).to.deep.equal({prop1: 'abc', prop2: 'def'});

const indexKeys = withScope.conversions.byAccessPattern.test.fromComposite.toKeys({prop1: 'abc', prop2: 'def'});
expect(indexKeys.pk).to.equal('$test_scope1#prop1_abc');

const indexKeysComposite = withScope.conversions.byAccessPattern.test.fromKeys.toComposite(indexKeys);
expect(indexKeysComposite).to.deep.equal({prop1: 'abc', prop2: 'def'});

const reverseKeys = withScope.conversions.byAccessPattern.reverse.fromComposite.toKeys({prop1: 'abc', prop2: 'def'});
expect(reverseKeys.gsi1pk).to.equal('$test_scope2#prop2_def');
expect(keys.pk).to.equal('$test_scope1#prop1_abc');

const reverseKeysComposite = withScope.conversions.byAccessPattern.reverse.fromKeys.toComposite(reverseKeys);
expect(reverseKeysComposite).to.deep.equal({prop1: 'abc', prop2: 'def'});
});

it('should query scoped indexes without issue', async () => {
const prop1 = uuid();
const prop2 = uuid();

const record1 = {
prop1,
prop2,
prop3: uuid(),
};

const record2 = {
prop1,
prop2,
prop3: uuid(),
};

const [
scopeRecord,
withoutScopeRecord
] = await Promise.all([
withScope.create(record1).go(),
withoutScope.create(record2).go(),
]);

expect(scopeRecord.data).to.deep.equal(record1);
expect(withoutScopeRecord.data).to.deep.equal(record2);

const scopeGet = await withScope.get({prop1, prop2}).go();
expect(scopeGet.data).to.deep.equal(record1);

const withoutScopeGet = await withoutScope.get({prop1, prop2}).go();
expect(withoutScopeGet.data).to.deep.equal(record2);

const scopeQuery = await withScope.query.test({prop1}).go();
expect(scopeQuery.data).to.deep.equal([record1]);

const withoutScopeQuery = await withoutScope.query.test({prop1}).go();
expect(withoutScopeQuery.data).to.deep.equal([record2]);

const reverseScopeQuery = await withScope.query.reverse({prop2}).go();
expect(reverseScopeQuery.data).to.deep.equal([record1]);

const reverseWithoutScopeQuery = await withoutScope.query.reverse({prop2}).go();
expect(reverseWithoutScopeQuery.data).to.deep.equal([record2]);

const batchGetScopeRecords = await withScope.get([{prop1, prop2}]).go();
expect(batchGetScopeRecords.data).to.deep.equal([record1]);

const batchGetWithoutScopeRecords = await withoutScope.get([{prop1, prop2}]).go();
expect(batchGetWithoutScopeRecords.data).to.deep.equal([record2]);

const updatedScopeRecord = await withScope.update({prop1, prop2}).set({prop4: ['updated1']}).go({response: 'all_new'});
expect(updatedScopeRecord.data).to.deep.equal({
...record1,
prop4: ['updated1'],
});

const updatedWithoutScopeRecord = await withoutScope.update({prop1, prop2}).set({prop4: ['updated2']}).go({response: 'all_new'});
expect(updatedWithoutScopeRecord.data).to.deep.equal({
...record2,
prop4: ['updated2'],
});

const patchedScopeRecord = await withScope.patch({prop1, prop2}).append({prop4: ['patched1']}).go({response: 'all_new'});
expect(patchedScopeRecord.data).to.deep.equal({
...record1,
prop4: ['updated1', 'patched1'],
});

const patchedWithoutScopeRecord = await withoutScope.patch({prop1, prop2}).append({prop4: ['patched2']}).go({response: 'all_new'});
expect(patchedWithoutScopeRecord.data).to.deep.equal({
...record2,
prop4: ['updated2', 'patched2'],
});

const upsertedScopeRecord = await withScope.upsert({prop1, prop2}).append({prop4: ['upserted1']}).go({response: 'all_new'});
expect(upsertedScopeRecord.data).to.deep.equal({
...record1,
prop4: ['updated1', 'patched1', 'upserted1'],
});

const upsertedWithoutScopeRecord = await withoutScope.upsert({prop1, prop2}).append({prop4: ['upserted2']}).go({response: 'all_new'});
expect(upsertedWithoutScopeRecord.data).to.deep.equal({
...record2,
prop4: ['updated2', 'patched2', 'upserted2'],
});
});
});


Loading
Loading