Skip to content

Commit

Permalink
#244 - use the same parameter types for `MessageStoreLevel.prototype.…
Browse files Browse the repository at this point in the history
…query` and `IndexLevel.prototype.query` (#257)

this makes the API simpler to understand as `ExactCriterion` and `RangeCriterion` are expressed in the types rather than two separate parameters

it also entirely removes any chance of clashing if the same property is provided in both `exactCriteria` and `rangeCriteria`
  • Loading branch information
dcrousso authored Mar 11, 2023
1 parent 019f9bf commit d21765b
Show file tree
Hide file tree
Showing 10 changed files with 60 additions and 218 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Decentralized Web Node (DWN) SDK

Code Coverage
![Statements](https://img.shields.io/badge/statements-94.79%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-92.95%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-93.36%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.79%25-brightgreen.svg?style=flat)
![Statements](https://img.shields.io/badge/statements-94.91%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-92.93%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-93.27%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.91%25-brightgreen.svg?style=flat)

## Introduction

Expand Down
12 changes: 12 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,15 @@ export type DataReferencingMessage = {

encodedData: string;
};

export type EqualFilter = string | number | boolean;

export type OneOfFilter = EqualFilter[];

type GT = ({ gt: string } & { gte?: never }) | ({ gt?: never } & { gte: string });
type LT = ({ lt: string } & { lte?: never }) | ({ lt?: never } & { lte: string });
export type RangeFilter = (GT | LT) & Partial<GT> & Partial<LT>;

export type Filter = {
[property: string]: EqualFilter | OneOfFilter | RangeFilter
};
49 changes: 12 additions & 37 deletions src/interfaces/records/handlers/records-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,13 @@ export class RecordsQueryHandler implements MethodHandler {
*/
private async fetchRecordsAsOwner(tenant: string, recordsQuery: RecordsQuery): Promise<BaseMessage[]> {
// fetch all published records matching the query
const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter);
const completeExactCriteria = {
...exactCriteria,
const filter = {
...RecordsQuery.convertFilter(recordsQuery.message.descriptor.filter),
interface : DwnInterfaceName.Records,
method : DwnMethodName.Write,
isLatestBaseState : true
};

const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter);
const records = await StorageController.query(this.messageStore, this.dataStore, tenant, completeExactCriteria, rangeCriteria);
const records = await StorageController.query(this.messageStore, this.dataStore, tenant, filter);
return records;
}

Expand All @@ -100,17 +97,14 @@ export class RecordsQueryHandler implements MethodHandler {
*/
private async fetchPublishedRecords(tenant: string, recordsQuery: RecordsQuery): Promise<BaseMessage[]> {
// fetch all published records matching the query
const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter);
const completeExactCriteria = {
...exactCriteria,
const filter = {
...RecordsQuery.convertFilter(recordsQuery.message.descriptor.filter),
interface : DwnInterfaceName.Records,
method : DwnMethodName.Write,
published : true,
isLatestBaseState : true
};

const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter);
const publishedRecords = await StorageController.query(this.messageStore, this.dataStore, tenant, completeExactCriteria, rangeCriteria);
const publishedRecords = await StorageController.query(this.messageStore, this.dataStore, tenant, filter);
return publishedRecords;
}

Expand All @@ -119,24 +113,15 @@ export class RecordsQueryHandler implements MethodHandler {
*/
private async fetchUnpublishedRecordsForRequester(tenant: string, recordsQuery: RecordsQuery): Promise<BaseMessage[]> {
// include records where recipient is requester
const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter);
const completeExactCriteria = {
...exactCriteria,
const filter = {
...RecordsQuery.convertFilter(recordsQuery.message.descriptor.filter),
interface : DwnInterfaceName.Records,
method : DwnMethodName.Write,
recipient : recordsQuery.author,
isLatestBaseState : true,
published : false
};

const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter);
const unpublishedRecordsForRequester = await StorageController.query(
this.messageStore,
this.dataStore,
tenant,
completeExactCriteria,
rangeCriteria
);
const unpublishedRecordsForRequester = await StorageController.query(this.messageStore, this.dataStore, tenant, filter);
return unpublishedRecordsForRequester;
}

Expand All @@ -145,25 +130,15 @@ export class RecordsQueryHandler implements MethodHandler {
*/
private async fetchUnpublishedRecordsByRequester(tenant: string, recordsQuery: RecordsQuery): Promise<BaseMessage[]> {
// include records where recipient is requester
const exactCriteria = RecordsQuery.getExactCriteria(recordsQuery.message.descriptor.filter);
const completeExactCriteria = {
...exactCriteria,
const filter = {
...RecordsQuery.convertFilter(recordsQuery.message.descriptor.filter),
author : recordsQuery.author,
interface : DwnInterfaceName.Records,
method : DwnMethodName.Write,
isLatestBaseState : true,
published : false
};

const rangeCriteria = RecordsQuery.getRangeCriteria(recordsQuery.message.descriptor.filter);

const unpublishedRecordsForRequester = await StorageController.query(
this.messageStore,
this.dataStore,
tenant,
completeExactCriteria,
rangeCriteria
);
const unpublishedRecordsForRequester = await StorageController.query(this.messageStore, this.dataStore, tenant, filter);
return unpublishedRecordsForRequester;
}
}
Expand Down
46 changes: 25 additions & 21 deletions src/interfaces/records/messages/records-query.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SignatureInput } from '../../../jose/jws/general/types.js';
import type { ExactCriterion, RangeCriterion, RecordsQueryDescriptor, RecordsQueryFilter, RecordsQueryMessage } from '../types.js';
import type { Filter, RangeFilter } from '../../../core/types.js';
import type { RecordsQueryDescriptor, RecordsQueryFilter, RecordsQueryMessage } from '../types.js';

import { getCurrentTimeInHighPrecision } from '../../../utils/time.js';
import { Message } from '../../../core/message.js';
Expand Down Expand Up @@ -70,25 +71,28 @@ export class RecordsQuery extends Message {
}
}

/**
* Gets the criteria for exact matches and exclude other types of criteria such as range criteria.
* @returns object contain all exact-match criteria; empty object if no exact-match criterion is found.
*/
public static getExactCriteria(filter: RecordsQueryFilter): { [key:string]: ExactCriterion } {
const filterCopy = { ... filter };
delete filterCopy.dateCreated;

removeUndefinedProperties(filterCopy);

return filterCopy as { [key:string]: ExactCriterion };
}

/**
* Gets the list of range criteria (e.g. `dateCreated`) and exclude other types of criteria such as exact matches.
* @returns object contain all range criteria; empty object if no range criterion is found.
*/
public static getRangeCriteria(filter: RecordsQueryFilter): { [key:string]: RangeCriterion } {
const rangeCriteria = filter.dateCreated ? { dateCreated: filter.dateCreated } : { };
return rangeCriteria;
public static convertFilter(filter: RecordsQueryFilter): Filter {
const result: Filter = { };
for (const key in filter) {
switch (key) {
case 'dateCreated':
var rangeFilter: RangeFilter = {
gte : filter.dateCreated.from,
lte : filter.dateCreated.to
};
if (rangeFilter.gte === undefined) {
delete rangeFilter.gte;
}
if (rangeFilter.lte === undefined) {
delete rangeFilter.lte;
}
result.dateCreated = rangeFilter;
break;
default:
result[key] = filter[key];
break;
}
}
return result;
}
}
8 changes: 0 additions & 8 deletions src/interfaces/records/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,6 @@ export type RecordsQueryFilter = {
dateCreated?: RangeCriterion;
};

/**
* An exact criterion in a query filter.
*/
export type ExactCriterion = unknown;

/**
* A range criterion in a query filter.
*/
export type RangeCriterion = {
/**
* Inclusive starting date-time.
Expand Down
13 changes: 1 addition & 12 deletions src/store/index-level.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { LevelDatabase } from './create-level.js';
import type { AbstractBatchDelOperation, AbstractBatchOperation, AbstractIteratorOptions } from 'abstract-level';
import type { Filter, RangeFilter } from '../core/types.js';

import { abortOr } from '../utils/abort.js';
import { createLevelDatabase } from './create-level.js';
Expand All @@ -10,18 +11,6 @@ export type Entry = {
[property: string]: unknown
};

export type EqualFilter = string | number | boolean;

export type OneOfFilter = EqualFilter[];

type GT = ({ gt: string } & { gte?: never }) | ({ gt?: never } & { gte: string });
type LT = ({ lt: string } & { lte?: never }) | ({ lt?: never } & { lte: string });
export type RangeFilter = (GT | LT) & Partial<GT> & Partial<LT>;

export type Filter = {
[property: string]: EqualFilter | OneOfFilter | RangeFilter
};

export interface IndexLevelOptions {
signal?: AbortSignal;
}
Expand Down
51 changes: 3 additions & 48 deletions src/store/message-store-level.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { BaseMessage } from '../core/types.js';
import type { Filter, RangeFilter } from './index-level.js';
import type { BaseMessage, Filter } from '../core/types.js';
import type { MessageStore, MessageStoreOptions } from './message-store.js';

import * as block from 'multiformats/block';
Expand All @@ -9,10 +8,8 @@ import { abortOr } from '../utils/abort.js';
import { BlockstoreLevel } from './blockstore-level.js';
import { CID } from 'multiformats/cid';
import { createLevelDatabase } from './create-level.js';
import { flatten } from '../utils/object.js';
import { IndexLevel } from './index-level.js';
import { sha256 } from 'multiformats/hashes/sha2';
import { ExactCriterion, RangeCriterion } from '../interfaces/records/types.js';

/**
* A simple implementation of {@link MessageStore} that works in both the browser and server-side.
Expand Down Expand Up @@ -79,21 +76,12 @@ export class MessageStoreLevel implements MessageStore {
return messageJson;
}

async query(
tenant: string,
exactCriteria: { [key: string]: ExactCriterion },
rangeCriteria?: { [key: string]: RangeCriterion },
options?: MessageStoreOptions
): Promise<BaseMessage[]> {
async query(tenant: string, filter: Filter, options?: MessageStoreOptions): Promise<BaseMessage[]> {
options?.signal?.throwIfAborted();

const messages: BaseMessage[] = [];

// parse criteria into a query that is compatible with the indexing DB we're using
const exactTerms = MessageStoreLevel.buildExactQueryTerms({ ...exactCriteria, tenant });
const rangeTerms = MessageStoreLevel.buildRangeQueryTerms(rangeCriteria);

const resultIds = await this.index.query({ ...rangeTerms, ...exactTerms }, options);
const resultIds = await this.index.query({ ...filter, tenant }, options);

for (const id of resultIds) {
const message = await this.get(tenant, id, options);
Expand Down Expand Up @@ -148,39 +136,6 @@ export class MessageStoreLevel implements MessageStore {
await this.blockstore.clear();
await this.index.clear();
}

private static buildExactQueryTerms(
exactCriteria: { [key: string]: ExactCriterion } = { }
): Filter {
return flatten(exactCriteria) as Filter;
}

private static buildRangeQueryTerms(
rangeCriteria: { [key: string]: RangeCriterion} = { }
): Filter {
const filter: Filter = { };

for (const propertyName in rangeCriteria) {
const rangeCriterion = rangeCriteria[propertyName];

const rangeFilter: RangeFilter = {
gte : rangeCriterion.from,
lte : rangeCriterion.to
};

if (rangeFilter.gte === undefined) {
delete rangeFilter.gte;
}

if (rangeFilter.lte === undefined) {
delete rangeFilter.lte;
}

filter[propertyName] = rangeFilter;
}

return filter;
}
}

type MessageStoreLevelConfig = {
Expand Down
16 changes: 3 additions & 13 deletions src/store/message-store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { BaseMessage } from '../core/types.js';
import type { ExactCriterion, RangeCriterion } from '../interfaces/records/types.js';
import type { BaseMessage, Filter } from '../core/types.js';

export interface MessageStoreOptions {
signal?: AbortSignal;
Expand Down Expand Up @@ -34,18 +33,9 @@ export interface MessageStore {
get(tenant: string, cid: string, options?: MessageStoreOptions): Promise<BaseMessage | undefined>;

/**
* Queries the underlying store for messages that match the query provided.
* The provided criteria are combined to form an AND filter for the query.
* Returns an empty array if no messages are found
* @param exactCriteria - criteria for exact matches
* @param rangeCriteria - criteria for range matches
* Queries the underlying store for messages that match the provided filter.
*/
query(
tenant: string,
exactCriteria: { [key: string]: ExactCriterion },
rangeCriteria?: { [key: string]: RangeCriterion },
options?: MessageStoreOptions
): Promise<BaseMessage[]>;
query(tenant: string, filter: Filter, options?: MessageStoreOptions ): Promise<BaseMessage[]>;

/**
* Deletes the message associated with the id provided.
Expand Down
8 changes: 3 additions & 5 deletions src/store/storage-controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { BaseMessage } from '../core/types.js';
import { DataStore } from './data-store.js';
import { MessageStore } from './message-store.js';
import { Readable } from 'readable-stream';
import { ExactCriterion, RangeCriterion } from '../interfaces/records/types.js';
import { BaseMessage, Filter } from '../core/types.js';

import { DataStream, Encoder } from '../index.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
Expand Down Expand Up @@ -80,11 +79,10 @@ export class StorageController {
messageStore: MessageStore,
dataStore: DataStore,
tenant: string,
exactCriteria: { [key: string]: ExactCriterion },
rangeCriteria?: { [key: string]: RangeCriterion }
filter: Filter
): Promise<BaseMessage[]> {

const messages = await messageStore.query(tenant, exactCriteria, rangeCriteria);
const messages = await messageStore.query(tenant, filter);

for (const message of messages) {
const dataCid = message.descriptor.dataCid;
Expand Down
Loading

0 comments on commit d21765b

Please sign in to comment.