1
1
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'
5
3
6
4
export interface SearchStreamsPermissionFilter {
7
5
userId : HexString
@@ -15,10 +13,11 @@ export interface SearchStreamsPermissionFilter {
15
13
16
14
export type InternalSearchStreamsPermissionFilter = ChangeFieldType < SearchStreamsPermissionFilter , 'userId' , UserID >
17
15
18
- export type SearchStreamsResultItem = {
16
+ export interface SearchStreamsResultItem {
19
17
id : string
20
- stream : StreamQueryResult
21
- } & ChainPermissions
18
+ metadata : string
19
+ permissions : ChainPermissions [ ]
20
+ }
22
21
23
22
export const toInternalSearchStreamsPermissionFilter = ( filter : SearchStreamsPermissionFilter ) : InternalSearchStreamsPermissionFilter => {
24
23
return {
@@ -32,101 +31,87 @@ export async function* searchStreams(
32
31
permissionFilter : InternalSearchStreamsPermissionFilter | undefined ,
33
32
theGraphClient : TheGraphClient ,
34
33
) : AsyncGenerator < SearchStreamsResultItem > {
35
- const backendResults = theGraphClient . queryEntities < SearchStreamsResultItem > (
34
+ yield * theGraphClient . queryEntities < SearchStreamsResultItem > (
36
35
( lastId : string , pageSize : number ) => buildQuery ( term , permissionFilter , lastId , pageSize )
37
36
)
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
+ }
51
38
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' )
69
47
}
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 ( ', ' ) } ]`
70
61
}
71
62
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
- */
79
63
const buildQuery = (
80
64
term : string | undefined ,
81
65
permissionFilter : InternalSearchStreamsPermissionFilter | undefined ,
82
66
lastId : string ,
83
67
pageSize : number
84
68
) : 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 ) } "` )
88
73
}
89
74
if ( permissionFilter !== undefined ) {
90
- variables . userId_in = [ permissionFilter . userId ]
75
+ const permissionExpressions : string [ ] = [ ]
76
+ const userId : string [ ] = [ permissionFilter . userId ]
91
77
if ( permissionFilter . allowPublic ) {
92
- variables . userId_in . push ( PUBLIC_PERMISSION_USER_ID )
78
+ userId . push ( PUBLIC_PERMISSION_USER_ID )
93
79
}
80
+ permissionExpressions . push ( `userId_in: [${ userId . map ( wrapWithQuotes ) . join ( ',' ) } ]` )
81
+ const nowTimestampInSeconds = Math . round ( Date . now ( ) / 1000 )
94
82
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 ) )
101
100
}
101
+ whereExpressions . push ( `permissions_: { and: [${ permissionExpressions . map ( wrapSubExpression ) . join ( ', ' ) } ] }` )
102
102
}
103
103
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
+ }
118
111
) {
119
112
id
120
- stream {
121
- id
122
- metadata
123
- }
124
- canEdit
125
- canDelete
126
- publishExpiration
127
- subscribeExpiration
128
- canGrant
113
+ metadata
129
114
}
130
115
}`
131
- return { query, variables }
116
+ return { query }
132
117
}
0 commit comments