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

EventsQuery by protocol #762

Merged
merged 9 commits into from
Jun 21, 2024
77 changes: 1 addition & 76 deletions json-schemas/interface-methods/events-filter.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,7 @@
"protocol": {
"type": "string"
},
"protocolPath": {
"type": "string"
},
"recipient": {
"$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/did"
},
"contextId": {
"type": "string"
},
"schema": {
"type": "string"
},
"recordId": {
"type": "string"
},
"parentId": {
"type": "string"
},
"dataFormat": {
"type": "string"
},
"dataSize": {
"$ref": "https://identity.foundation/dwn/json-schemas/number-range-filter.json"
},
"dateCreated": {
"messageTimestamp": {
"type": "object",
"minProperties": 1,
"additionalProperties": false,
Expand All @@ -59,57 +35,6 @@
"$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time"
}
}
},
"datePublished": {
"type": "object",
"minProperties": 1,
"additionalProperties": false,
"properties": {
"from": {
"$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time"
},
"to": {
"$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time"
}
}
},
"dateUpdated": {
"type": "object",
"minProperties": 1,
"additionalProperties": false,
"properties": {
"from": {
"$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time"
},
"to": {
"$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time"
}
}
}
},
"dependencies": {
"datePublished": {
"oneOf": [
{
"properties": {
"published": {
"enum": [
true
]
}
},
"required": [
"published"
]
},
{
"not": {
"required": [
"published"
]
}
}
]
}
}
}
4 changes: 2 additions & 2 deletions json-schemas/interface-methods/events-query.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"required": [
"interface",
"method",
"messageTimestamp"
"messageTimestamp",
"filters"
],
"properties": {
"interface": {
Expand All @@ -37,7 +38,6 @@
},
"filters": {
"type": "array",
"minItems": 1,
LiranCohen marked this conversation as resolved.
Show resolved Hide resolved
"items": {
"$ref": "https://identity.foundation/dwn/json-schemas/events-filter.json"
}
Expand Down
7 changes: 2 additions & 5 deletions src/interfaces/events-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { Events } from '../utils/events.js';
import { Message } from '../core/message.js';
import { removeUndefinedProperties } from '../utils/object.js';
import { Time } from '../utils/time.js';
import { validateProtocolUrlNormalized } from '../utils/url.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
import { validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../utils/url.js';

export type EventsQueryOptions = {
signer: Signer;
Expand All @@ -27,9 +27,6 @@ export class EventsQuery extends AbstractMessage<EventsQueryMessage>{
if ('protocol' in filter && filter.protocol !== undefined) {
validateProtocolUrlNormalized(filter.protocol);
}
if ('schema' in filter && filter.schema !== undefined) {
validateSchemaUrlNormalized(filter.schema);
}
}

return new EventsQuery(message);
Expand All @@ -39,7 +36,7 @@ export class EventsQuery extends AbstractMessage<EventsQueryMessage>{
const descriptor: EventsQueryDescriptor = {
interface : DwnInterfaceName.Events,
method : DwnMethodName.Query,
filters : options.filters ? Events.normalizeFilters(options.filters) : undefined,
filters : options.filters ? Events.normalizeFilters(options.filters) : [],
LiranCohen marked this conversation as resolved.
Show resolved Hide resolved
messageTimestamp : options.messageTimestamp ?? Time.getCurrentTimestamp(),
cursor : options.cursor,
};
Expand Down
5 changes: 1 addition & 4 deletions src/interfaces/events-subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { AbstractMessage } from '../core/abstract-message.js';
import { Message } from '../core/message.js';
import { removeUndefinedProperties } from '../utils/object.js';
import { Time } from '../utils/time.js';
import { validateProtocolUrlNormalized } from '../utils/url.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
import { validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../utils/url.js';


export type EventsSubscribeOptions = {
Expand All @@ -24,9 +24,6 @@ export class EventsSubscribe extends AbstractMessage<EventsSubscribeMessage> {
if ('protocol' in filter && filter.protocol !== undefined) {
validateProtocolUrlNormalized(filter.protocol);
}
if ('schema' in filter && filter.schema !== undefined) {
validateSchemaUrlNormalized(filter.schema);
}
}

Time.validateTimestamp(message.descriptor.messageTimestamp);
Expand Down
30 changes: 4 additions & 26 deletions src/types/events-types.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,17 @@
import type { MessageEvent } from './subscriptions.js';
import type { AuthorizationModel, GenericMessage, GenericMessageReply, MessageSubscription } from './message-types.js';
import type { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
import type { PaginationCursor, RangeCriterion, RangeFilter } from './query-types.js';
import type { PaginationCursor, RangeCriterion } from './query-types.js';
/**
* filters used when filtering for any type of Message across interfaces
*/
export type EventsMessageFilter = {
export type EventsFilter = {
interface?: string;
method?: string;
dateUpdated?: RangeCriterion;
};

/**
* We only allow filtering for events by immutable properties, the omitted properties could be different per subsequent writes.
*/
export type EventsRecordsFilter = {
recipient?: string;
protocol?: string;
protocolPath?: string;
contextId?: string;
schema?: string;
recordId?: string;
parentId?: string;
dataFormat?: string;
dataSize?: RangeFilter;
dateCreated?: RangeCriterion;
messageTimestamp?: RangeCriterion;
};


/**
* A union type of the different types of filters a user can use when issuing an EventsQuery or EventsSubscribe
* TODO: simplify the EventsFilters to only the necessary in order to reduce complexity https://github.com/TBD54566975/dwn-sdk-js/issues/663
*/
export type EventsFilter = EventsMessageFilter | EventsRecordsFilter;

export type MessageSubscriptionHandler = (event: MessageEvent) => void;

export type EventsSubscribeMessageOptions = {
Expand All @@ -60,7 +38,7 @@ export type EventsQueryDescriptor = {
interface: DwnInterfaceName.Events;
method: DwnMethodName.Query;
messageTimestamp: string;
filters?: EventsFilter[];
filters: EventsFilter[];
LiranCohen marked this conversation as resolved.
Show resolved Hide resolved
cursor?: PaginationCursor;
};

Expand Down
71 changes: 40 additions & 31 deletions src/utils/events.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { EventsFilter } from '../types/events-types.js';
import type { Filter } from '../types/query-types.js';
import type { EventsFilter, EventsMessageFilter, EventsRecordsFilter } from '../types/events-types.js';

import { FilterUtility } from '../utils/filter.js';
import { normalizeProtocolUrl } from './url.js';
import { PermissionsProtocol } from '../protocols/permissions.js';
import { Records } from '../utils/records.js';
import { isEmptyObject, removeUndefinedProperties } from './object.js';

Expand All @@ -19,13 +21,13 @@ export class Events {

// normalize each filter individually by the type of filter it is.
thehenrytsai marked this conversation as resolved.
Show resolved Hide resolved
for (const filter of filters) {
let eventsFilter: EventsFilter;
if (this.isRecordsFilter(filter)) {
eventsFilter = Records.normalizeFilter(filter);
} else {
// no normalization needed
eventsFilter = filter;
}
// normalize the protocol URL if it exists
const protocol = filter.protocol !== undefined ? normalizeProtocolUrl(filter.protocol) : undefined;

const eventsFilter = {
...filter,
protocol,
};

// remove any empty filter properties and do not add if empty
removeUndefinedProperties(eventsFilter);
Expand All @@ -34,7 +36,6 @@ export class Events {
}
}


return eventsQueryFilters;
}

Expand All @@ -53,43 +54,51 @@ export class Events {
// first we check for `EventsRecordsFilter` fields for conversion
// otherwise it is `EventsMessageFilter` fields for conversion
for (const filter of filters) {
if (this.isRecordsFilter(filter)) {
eventsQueryFilters.push(Records.convertFilter(filter));
} else {
eventsQueryFilters.push(this.convertFilter(filter));
// extract the protocol tag filter from the incoming event record filter
// this filters for permission grants, requests and revocations associated with a targeted protocol
// since permissions are their own protocol, we add an additional tag index when writing the permission messages, so we can filter on it here
thehenrytsai marked this conversation as resolved.
Show resolved Hide resolved
const protocolTagFilter = this.extractProtocolTagFilters(filter);
thehenrytsai marked this conversation as resolved.
Show resolved Hide resolved
if (protocolTagFilter) {
eventsQueryFilters.push(protocolTagFilter);
}

eventsQueryFilters.push(this.convertFilter(filter));
}

return eventsQueryFilters;
}

private static extractProtocolTagFilters(filter: EventsFilter): Filter | undefined {
LiranCohen marked this conversation as resolved.
Show resolved Hide resolved
const { protocol, messageTimestamp } = filter;
if (protocol !== undefined) {
const taggedFilter = {
protocol: PermissionsProtocol.uri,
...Records.convertTagsFilter({ protocol })
} as Filter;

if (messageTimestamp != undefined) {
const messageTimestampFilter = FilterUtility.convertRangeCriterion(messageTimestamp);
if (messageTimestampFilter) {
taggedFilter.messageTimestamp = messageTimestampFilter;
}
}

return taggedFilter;
}
}

/**
* Converts an external-facing filter model into an internal-facing filer model used by data store.
*/
private static convertFilter(filter: EventsMessageFilter): Filter {
private static convertFilter(filter: EventsFilter): Filter {
const filterCopy = { ...filter } as Filter;

const { dateUpdated } = filter;
const messageTimestampFilter = dateUpdated ? FilterUtility.convertRangeCriterion(dateUpdated) : undefined;
const { messageTimestamp } = filter;
const messageTimestampFilter = messageTimestamp ? FilterUtility.convertRangeCriterion(messageTimestamp) : undefined;
if (messageTimestampFilter) {
filterCopy.messageTimestamp = messageTimestampFilter;
delete filterCopy.dateUpdated;
}
return filterCopy as Filter;
}

// we deliberately do not check for `dateUpdated` in this filter.
// if it were the only property that matched, it could be handled by `EventsFilter`
private static isRecordsFilter(filter: EventsFilter): filter is EventsRecordsFilter {
return 'author' in filter ||
'dateCreated' in filter ||
'dataFormat' in filter ||
'dataSize' in filter ||
'parentId' in filter ||
'recordId' in filter ||
'schema' in filter ||
'protocol' in filter ||
'protocolPath' in filter ||
'recipient' in filter;
}
}
2 changes: 1 addition & 1 deletion src/utils/records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export class Records {
/**
* This will create individual keys for each of the tag filters that look like `tag.tag_filter_property`
*/
private static convertTagsFilter( tags: { [property: string]: RecordsWriteTagsFilter}): Filter {
public static convertTagsFilter( tags: { [property: string]: RecordsWriteTagsFilter}): Filter {
const tagValues:Filter = {};
for (const property in tags) {
const value = tags[property];
Expand Down
23 changes: 3 additions & 20 deletions tests/handlers/events-query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ export function testEventsQueryHandler(): void {
const bob = await TestDataGenerator.generateDidKeyPersona();

const { message } = await TestDataGenerator.generateEventsQuery({
author : alice,
filters : [{ schema: 'schema1' }]
author: alice,
});
const eventsQueryHandler = new EventsQueryHandler(didResolver, eventLog);
const reply = await eventsQueryHandler.handle({ tenant: bob.did, message });
Expand All @@ -76,8 +75,7 @@ export function testEventsQueryHandler(): void {
const alice = await TestDataGenerator.generateDidKeyPersona();

const { message } = await TestDataGenerator.generateEventsQuery({
author : alice,
filters : [{ schema: 'schema1' }]
author: alice,
});
(message['descriptor'] as any)['troll'] = 'hehe';

Expand All @@ -88,27 +86,12 @@ export function testEventsQueryHandler(): void {
expect(reply.entries).to.not.exist;
});

it('returns 400 if no filters are provided', async () => {
LiranCohen marked this conversation as resolved.
Show resolved Hide resolved
const alice = await TestDataGenerator.generateDidKeyPersona();

const { message } = await TestDataGenerator.generateEventsQuery({
author : alice,
filters : [{ schema: 'schema1' }],
}); // create with filter to prevent failure on .create()
message.descriptor.filters = []; // remove filters
const eventsQueryHandler = new EventsQueryHandler(didResolver, eventLog);
const reply = await eventsQueryHandler.handle({ tenant: alice.did, message });

expect(reply.status.code).to.equal(400);
expect(reply.entries).to.not.exist;
});

it('returns 400 if an empty filter without properties is provided', async () => {
const alice = await TestDataGenerator.generateDidKeyPersona();

const { message } = await TestDataGenerator.generateEventsQuery({
author : alice,
filters : [{ schema: 'schema1' }],
filters : [{ protocol: 'http://example.org/protocol/v1' }],
}); // create with filter to prevent failure on .create()
message.descriptor.filters = [{}]; // empty out filter properties
const eventsQueryHandler = new EventsQueryHandler(didResolver, eventLog);
Expand Down
Loading