Skip to content

Commit cb3eda2

Browse files
committed
query the results directly from the Stream entity instead of using StreamPermission entitity
1 parent ca55593 commit cb3eda2

File tree

3 files changed

+66
-100
lines changed

3 files changed

+66
-100
lines changed

packages/sdk/src/contracts/StreamRegistry.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,6 @@ import { ContractFactory } from './ContractFactory'
4747
import { ObservableContract, initContractEventGateway, waitForTx } from './contract'
4848
import { InternalSearchStreamsPermissionFilter, searchStreams as _searchStreams } from './searchStreams'
4949

50-
/*
51-
* On-chain registry of stream metadata and permissions.
52-
*
53-
* Does not support system streams (the key exchange stream)
54-
*/
55-
56-
export interface StreamQueryResult {
57-
id: string
58-
metadata: string
59-
}
60-
6150
interface StreamPublisherOrSubscriberItem {
6251
id: string
6352
userId: string
@@ -303,8 +292,8 @@ export class StreamRegistry {
303292
permissionFilter,
304293
this.theGraphClient)
305294
for await (const item of queryResult) {
306-
const id = toStreamID(item.stream.id)
307-
this.populateMetadataCache(id, parseMetadata(item.stream.metadata))
295+
const id = toStreamID(item.id)
296+
this.populateMetadataCache(id, parseMetadata(item.metadata))
308297
yield id
309298
}
310299
}
Lines changed: 64 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { ChangeFieldType, GraphQLQuery, HexString, TheGraphClient, toUserId, UserID } from '@streamr/utils'
2-
import { ChainPermissions, convertChainPermissionsToStreamPermissions, PUBLIC_PERMISSION_USER_ID, StreamPermission } from '../permission'
3-
import { filter, unique } from '../utils/GeneratorUtils'
4-
import { StreamQueryResult } from './StreamRegistry'
2+
import { ChainPermissions, PUBLIC_PERMISSION_USER_ID, StreamPermission } from '../permission'
53

64
export interface SearchStreamsPermissionFilter {
75
userId: HexString
@@ -15,10 +13,11 @@ export interface SearchStreamsPermissionFilter {
1513

1614
export type InternalSearchStreamsPermissionFilter = ChangeFieldType<SearchStreamsPermissionFilter, 'userId', UserID>
1715

18-
export type SearchStreamsResultItem = {
16+
export interface SearchStreamsResultItem {
1917
id: string
20-
stream: StreamQueryResult
21-
} & ChainPermissions
18+
metadata: string
19+
permissions: ChainPermissions[]
20+
}
2221

2322
export const toInternalSearchStreamsPermissionFilter = (filter: SearchStreamsPermissionFilter): InternalSearchStreamsPermissionFilter => {
2423
return {
@@ -32,101 +31,87 @@ export async function* searchStreams(
3231
permissionFilter: InternalSearchStreamsPermissionFilter | undefined,
3332
theGraphClient: TheGraphClient,
3433
): AsyncGenerator<SearchStreamsResultItem> {
35-
const backendResults = theGraphClient.queryEntities<SearchStreamsResultItem>(
34+
yield* theGraphClient.queryEntities<SearchStreamsResultItem>(
3635
(lastId: string, pageSize: number) => buildQuery(term, permissionFilter, lastId, pageSize)
3736
)
38-
/*
39-
* There can be orphaned permission entities if a stream is deleted (currently
40-
* we don't remove the assigned permissions, see ETH-222)
41-
* TODO remove the filtering when ETH-222 has been implemented
42-
*/
43-
const withoutOrphaned = filter(backendResults, (p) => p.stream !== null)
44-
/*
45-
* As we query via permissions entity, any stream can appear multiple times (once per
46-
* permission user) if we don't do have exactly one userId in the GraphQL query.
47-
* That is the case if no permission filter is defined at all, or if permission.allowPublic
48-
* is true (then it appears twice: once for the user, and once for the public address).
49-
*/
50-
const withoutDuplicates = unique(withoutOrphaned, (p) => p.stream.id)
37+
}
5138

52-
if (permissionFilter !== undefined) {
53-
/*
54-
* There are situations where the The Graph may contain empty assignments (all boolean flags false,
55-
* and all expirations in the past). E.g.:
56-
* - if we granted some permissions to a user, but then removed all those permissions
57-
* - if we granted an expirable permission (subscribe or publish), and it has now expired
58-
* We don't want to return empty assignments to the user, because from user's perspective those are
59-
* non-existing assignments.
60-
* -> Here we filter out the empty assignments by defining a fallback value for anyOf filter
61-
*/
62-
const anyOf = permissionFilter.anyOf ?? Object.values(StreamPermission) as StreamPermission[]
63-
yield* filter(withoutDuplicates, (item: SearchStreamsResultItem) => {
64-
const actual = convertChainPermissionsToStreamPermissions(item)
65-
return anyOf.some((p) => actual.includes(p))
66-
})
67-
} else {
68-
yield* withoutDuplicates
39+
const escapeStringValue = (s: string) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') // escape backslashes and double quotes
40+
const wrapWithQuotes = (s: string) => `"${s}"`
41+
const wrapSubExpression = (s: string) => `{ ${s} }`
42+
43+
const createPermissionFilterExpression = (permissions: StreamPermission[], operator: 'and' | 'or', nowTimestampInSeconds: number) => {
44+
const subExpressions: string[] = []
45+
if (permissions.includes(StreamPermission.EDIT)) {
46+
subExpressions.push('canEdit: true')
6947
}
48+
if (permissions.includes(StreamPermission.DELETE)) {
49+
subExpressions.push('canDelete: true')
50+
}
51+
if (permissions.includes(StreamPermission.PUBLISH)) {
52+
subExpressions.push(`publishExpiration_gt: "${nowTimestampInSeconds}"`)
53+
}
54+
if (permissions.includes(StreamPermission.SUBSCRIBE)) {
55+
subExpressions.push(`subscribeExpiration_gt: "${nowTimestampInSeconds}"`)
56+
}
57+
if (permissions.includes(StreamPermission.GRANT)) {
58+
subExpressions.push('canGrant: true')
59+
}
60+
return `${operator}: [${subExpressions.map(wrapSubExpression).join(', ')}]`
7061
}
7162

72-
/*
73-
* Note that we query the results via permissions entity even if there is no permission filter
74-
* defined. It is maybe possible to optimize the non-permission related queries by searching over
75-
* the Stream entity. To support that we'd need to add a new field to The Graph (e.g. "idAsString"),
76-
* as we can't do substring filtering by Stream id field (there is no "id_contains" because
77-
* ID type is not a string)
78-
*/
7963
const buildQuery = (
8064
term: string | undefined,
8165
permissionFilter: InternalSearchStreamsPermissionFilter | undefined,
8266
lastId: string,
8367
pageSize: number
8468
): GraphQLQuery => {
85-
const variables: Record<string, any> = {
86-
stream_contains: term,
87-
id_gt: lastId
69+
const whereExpressions: string[] = []
70+
whereExpressions.push(`id_gt: "${escapeStringValue(lastId)}"`)
71+
if (term !== undefined) {
72+
whereExpressions.push(`idAsString_contains: "${escapeStringValue(term)}"`)
8873
}
8974
if (permissionFilter !== undefined) {
90-
variables.userId_in = [permissionFilter.userId]
75+
const permissionExpressions: string[] = []
76+
const userId: string[] = [permissionFilter.userId]
9177
if (permissionFilter.allowPublic) {
92-
variables.userId_in.push(PUBLIC_PERMISSION_USER_ID)
78+
userId.push(PUBLIC_PERMISSION_USER_ID)
9379
}
80+
permissionExpressions.push(`userId_in: [${userId.map(wrapWithQuotes).join(',')}]`)
81+
const nowTimestampInSeconds = Math.round(Date.now() / 1000)
9482
if (permissionFilter.allOf !== undefined) {
95-
const now = String(Math.round(Date.now() / 1000))
96-
variables.canEdit = permissionFilter.allOf.includes(StreamPermission.EDIT) ? true : undefined
97-
variables.canDelete = permissionFilter.allOf.includes(StreamPermission.DELETE) ? true : undefined
98-
variables.publishExpiration_gt = permissionFilter.allOf.includes(StreamPermission.PUBLISH) ? now : undefined
99-
variables.subscribeExpiration_gt = permissionFilter.allOf.includes(StreamPermission.SUBSCRIBE) ? now : undefined
100-
variables.canGrant = permissionFilter.allOf.includes(StreamPermission.GRANT) ? true : undefined
83+
permissionExpressions.push(createPermissionFilterExpression(permissionFilter.allOf, 'and', nowTimestampInSeconds))
84+
}
85+
/*
86+
* There are situations where the The Graph may contain empty assignments (all boolean flags false,
87+
* and all expirations in the past). E.g.:
88+
* - if we granted some permissions to a user, but then removed all those permissions
89+
* - if we granted an expirable permission (subscribe or publish), and it has now expired
90+
* We don't want to return empty assignments to the user, because from user's perspective those are
91+
* non-existing assignments. That's why we apply this extra virtual anyOf filter if none of the user-given
92+
* permission filters limit the result set in any way.
93+
*/
94+
const anyOfFilter = permissionFilter.anyOf
95+
?? (((permissionFilter.allOf === undefined) || (permissionFilter.allOf.length === 0))
96+
? Object.values(StreamPermission) as StreamPermission[]
97+
: undefined)
98+
if (anyOfFilter !== undefined) {
99+
permissionExpressions.push(createPermissionFilterExpression(anyOfFilter, 'or', nowTimestampInSeconds))
101100
}
101+
whereExpressions.push(`permissions_: { and: [${permissionExpressions.map(wrapSubExpression).join(', ')}] }`)
102102
}
103103
const query = `
104-
query (
105-
$stream_contains: String,
106-
$userId_in: [Bytes!]
107-
$canEdit: Boolean
108-
$canDelete: Boolean
109-
$publishExpiration_gt: BigInt
110-
$subscribeExpiration_gt: BigInt
111-
$canGrant: Boolean
112-
$id_gt: String
113-
) {
114-
streamPermissions (
115-
first: ${pageSize},
116-
orderBy: "stream__id",
117-
${TheGraphClient.createWhereClause(variables)}
104+
query {
105+
streams (
106+
first: ${pageSize}
107+
orderBy: "id"
108+
where: {
109+
${whereExpressions.join(', ')}
110+
}
118111
) {
119112
id
120-
stream {
121-
id
122-
metadata
123-
}
124-
canEdit
125-
canDelete
126-
publishExpiration
127-
subscribeExpiration
128-
canGrant
113+
metadata
129114
}
130115
}`
131-
return { query, variables }
116+
return { query }
132117
}

packages/utils/src/TheGraphClient.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,6 @@ export class TheGraphClient {
113113
// eslint-disable-next-line no-underscore-dangle
114114
return response._meta.block.number
115115
}
116-
117-
static createWhereClause(variables: Record<string, any>): string {
118-
const parameterList = Object.keys(variables)
119-
.filter((k) => variables[k] !== undefined)
120-
.map((k) => k + ': $' + k)
121-
.join(' ')
122-
return `where: { ${parameterList} }`
123-
}
124116
}
125117

126118
class BlockNumberGate extends Gate {

0 commit comments

Comments
 (0)