@contember/engine-content-api 1.0.0-rc.8 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/acl/Authorizator.d.ts +8 -3
- package/dist/src/acl/Authorizator.d.ts.map +1 -1
- package/dist/src/acl/Authorizator.js +46 -0
- package/dist/src/acl/Authorizator.js.map +1 -1
- package/dist/src/acl/index.d.ts +0 -1
- package/dist/src/acl/index.d.ts.map +1 -1
- package/dist/src/acl/index.js +0 -1
- package/dist/src/acl/index.js.map +1 -1
- package/dist/src/extensions/paginatedHasMany/PaginatedHasManyFieldProviderVisitor.d.ts.map +1 -1
- package/dist/src/extensions/paginatedHasMany/PaginatedHasManyFieldProviderVisitor.js +2 -0
- package/dist/src/extensions/paginatedHasMany/PaginatedHasManyFieldProviderVisitor.js.map +1 -1
- package/dist/src/introspection/IntrospectionSchemaFactory.js +2 -2
- package/dist/src/introspection/IntrospectionSchemaFactory.js.map +1 -1
- package/dist/src/mapper/Mapper.d.ts.map +1 -1
- package/dist/src/mapper/Mapper.js +2 -4
- package/dist/src/mapper/Mapper.js.map +1 -1
- package/dist/src/mapper/select/OrderByBuilder.js +1 -1
- package/dist/src/mapper/select/OrderByBuilder.js.map +1 -1
- package/dist/src/mapper/select/SelectBuilder.d.ts +1 -1
- package/dist/src/mapper/select/SelectBuilder.d.ts.map +1 -1
- package/dist/src/mapper/select/SelectBuilder.js +4 -7
- package/dist/src/mapper/select/SelectBuilder.js.map +1 -1
- package/dist/src/resolvers/MutationResolver.d.ts.map +1 -1
- package/dist/src/resolvers/MutationResolver.js +4 -3
- package/dist/src/resolvers/MutationResolver.js.map +1 -1
- package/dist/src/schema/EntityTypeProvider.js +3 -3
- package/dist/src/schema/EntityTypeProvider.js.map +1 -1
- package/dist/src/schema/FieldAccessVisitor.js +2 -2
- package/dist/src/schema/FieldAccessVisitor.js.map +1 -1
- package/dist/src/schema/MutationProvider.js +2 -2
- package/dist/src/schema/MutationProvider.js.map +1 -1
- package/dist/src/schema/QueryProvider.js +1 -1
- package/dist/src/schema/QueryProvider.js.map +1 -1
- package/dist/src/schema/WhereTypeProvider.js +2 -2
- package/dist/src/schema/WhereTypeProvider.js.map +1 -1
- package/dist/src/schema/entities/FieldTypeVisitor.d.ts +5 -3
- package/dist/src/schema/entities/FieldTypeVisitor.d.ts.map +1 -1
- package/dist/src/schema/entities/FieldTypeVisitor.js +13 -2
- package/dist/src/schema/entities/FieldTypeVisitor.js.map +1 -1
- package/dist/src/schema/mutations/CreateEntityInputFieldVisitor.js +1 -1
- package/dist/src/schema/mutations/CreateEntityInputFieldVisitor.js.map +1 -1
- package/dist/src/schema/mutations/CreateEntityRelationAllowedOperationsVisitor.js +3 -3
- package/dist/src/schema/mutations/CreateEntityRelationAllowedOperationsVisitor.js.map +1 -1
- package/dist/src/schema/mutations/EntityInputProvider.js +1 -1
- package/dist/src/schema/mutations/EntityInputProvider.js.map +1 -1
- package/dist/src/schema/mutations/UpdateEntityInputFieldVisitor.js +1 -1
- package/dist/src/schema/mutations/UpdateEntityInputFieldVisitor.js.map +1 -1
- package/dist/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.d.ts.map +1 -1
- package/dist/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.js +5 -5
- package/dist/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.js.map +1 -1
- package/dist/src/schema/mutations/UpdateEntityRelationInputFieldVisitor.js +1 -1
- package/dist/src/schema/mutations/UpdateEntityRelationInputFieldVisitor.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/tests/cases/integration/graphQlRequests/query/relations/oneHasMany.test.js +87 -0
- package/dist/tests/cases/integration/graphQlRequests/query/relations/oneHasMany.test.js.map +1 -1
- package/dist/tests/cases/integration/graphQlRequests/upsert/basicUpsert.test.js +45 -2
- package/dist/tests/cases/integration/graphQlRequests/upsert/basicUpsert.test.js.map +1 -1
- package/dist/tests/cases/integration/graphQlSchema/graphQlSchemaBuilder.test.js +45 -1
- package/dist/tests/cases/integration/graphQlSchema/graphQlSchemaBuilder.test.js.map +1 -1
- package/dist/tests/cases/integration/graphQlSchema/schema-acl-predicate.gql +110 -0
- package/dist/tests/cases/integration/graphQlSchema/schema-acl-predicate2.gql +110 -0
- package/dist/tests/src/test.js +1 -1
- package/dist/tests/src/test.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/acl/Authorizator.ts +48 -4
- package/src/acl/index.ts +0 -1
- package/src/extensions/paginatedHasMany/PaginatedHasManyFieldProviderVisitor.ts +2 -0
- package/src/introspection/IntrospectionSchemaFactory.ts +2 -2
- package/src/mapper/Mapper.ts +2 -5
- package/src/mapper/select/OrderByBuilder.ts +1 -1
- package/src/mapper/select/SelectBuilder.ts +4 -8
- package/src/resolvers/MutationResolver.ts +4 -3
- package/src/schema/EntityTypeProvider.ts +3 -3
- package/src/schema/FieldAccessVisitor.ts +2 -2
- package/src/schema/MutationProvider.ts +2 -2
- package/src/schema/QueryProvider.ts +1 -1
- package/src/schema/WhereTypeProvider.ts +2 -2
- package/src/schema/entities/FieldTypeVisitor.ts +17 -8
- package/src/schema/mutations/CreateEntityInputFieldVisitor.ts +1 -1
- package/src/schema/mutations/CreateEntityRelationAllowedOperationsVisitor.ts +3 -3
- package/src/schema/mutations/EntityInputProvider.ts +1 -1
- package/src/schema/mutations/UpdateEntityInputFieldVisitor.ts +1 -1
- package/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.ts +5 -9
- package/src/schema/mutations/UpdateEntityRelationInputFieldVisitor.ts +1 -1
- package/tests/cases/integration/graphQlRequests/query/relations/oneHasMany.test.ts +89 -0
- package/tests/cases/integration/graphQlRequests/upsert/basicUpsert.test.ts +49 -2
- package/tests/cases/integration/graphQlSchema/graphQlSchemaBuilder.test.ts +57 -2
- package/tests/cases/integration/graphQlSchema/schema-acl-predicate.gql +110 -0
- package/tests/cases/integration/graphQlSchema/schema-acl-predicate2.gql +110 -0
- package/tests/src/test.ts +2 -2
- package/dist/src/acl/StaticAuthorizator.d.ts +0 -9
- package/dist/src/acl/StaticAuthorizator.d.ts.map +0 -1
- package/dist/src/acl/StaticAuthorizator.js +0 -32
- package/dist/src/acl/StaticAuthorizator.js.map +0 -1
- package/src/acl/StaticAuthorizator.ts +0 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/engine-content-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"typings": "dist/src/index.d.ts",
|
|
@@ -10,19 +10,19 @@
|
|
|
10
10
|
"test": "uvu dist/tests/cases/ \\.js$"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@contember/database": "^1.0.
|
|
14
|
-
"@contember/dic": "^1.0.
|
|
15
|
-
"@contember/engine-common": "^1.0.
|
|
16
|
-
"@contember/graphql-utils": "^1.0.
|
|
17
|
-
"@contember/schema": "^1.0.
|
|
18
|
-
"@contember/schema-utils": "^1.0.
|
|
13
|
+
"@contember/database": "^1.0.1",
|
|
14
|
+
"@contember/dic": "^1.0.1",
|
|
15
|
+
"@contember/engine-common": "^1.0.1",
|
|
16
|
+
"@contember/graphql-utils": "^1.0.1",
|
|
17
|
+
"@contember/schema": "^1.0.1",
|
|
18
|
+
"@contember/schema-utils": "^1.0.1",
|
|
19
19
|
"@graphql-tools/schema": "^7.1.5",
|
|
20
20
|
"graphql-tag": "^2.12.5"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@contember/database-tester": "^1.0.
|
|
24
|
-
"@contember/engine-api-tester": "^1.0.
|
|
25
|
-
"@contember/schema-definition": "^1.0.
|
|
23
|
+
"@contember/database-tester": "^1.0.1",
|
|
24
|
+
"@contember/engine-api-tester": "^1.0.1",
|
|
25
|
+
"@contember/schema-definition": "^1.0.1",
|
|
26
26
|
"@graphql-codegen/cli": "^1.15.2",
|
|
27
27
|
"@graphql-codegen/typescript": "^1.15.2",
|
|
28
28
|
"@graphql-codegen/typescript-operations": "^1.15.2",
|
package/src/acl/Authorizator.ts
CHANGED
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
import { Acl } from '@contember/schema'
|
|
2
2
|
|
|
3
|
-
export interface Authorizator {
|
|
4
|
-
isAllowed(operation: Acl.Operation, entity: string): boolean
|
|
5
|
-
isAllowed(operation: Acl.Operation.create | Acl.Operation.read | Acl.Operation.update, entity: string, field: string,): boolean
|
|
6
3
|
|
|
7
|
-
|
|
4
|
+
export type AuthorizationResult = 'yes' | 'no' | 'maybe'
|
|
5
|
+
|
|
6
|
+
export class Authorizator {
|
|
7
|
+
constructor(private readonly permissions: Acl.Permissions) {
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getEntityPermission(operation: Acl.Operation, entity: string): AuthorizationResult {
|
|
11
|
+
if (!this.permissions[entity]) {
|
|
12
|
+
return 'no'
|
|
13
|
+
}
|
|
14
|
+
const entityPermissions: Acl.EntityPermissions = this.permissions[entity]
|
|
15
|
+
|
|
16
|
+
if (operation === Acl.Operation.delete) {
|
|
17
|
+
return this.predicateToResult(entityPermissions.operations.delete)
|
|
18
|
+
}
|
|
19
|
+
const fieldPermissions = entityPermissions.operations[operation]
|
|
20
|
+
if (!fieldPermissions) {
|
|
21
|
+
return 'no'
|
|
22
|
+
}
|
|
23
|
+
return Object.values(fieldPermissions).filter(it => !!it).length > 0 ? 'yes' : 'no'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getFieldPredicate(operation: Acl.Operation.create | Acl.Operation.read | Acl.Operation.update, entity: string, field: string): Acl.Predicate {
|
|
27
|
+
return this.permissions[entity]?.operations[operation]?.[field] ?? false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getFieldPermissions(operation: Acl.Operation.create | Acl.Operation.read | Acl.Operation.update, entity: string, field: string): AuthorizationResult {
|
|
31
|
+
const fieldPermissions = this.getFieldPredicate(operation, entity, field)
|
|
32
|
+
if (!fieldPermissions) {
|
|
33
|
+
return 'no'
|
|
34
|
+
}
|
|
35
|
+
return this.predicateToResult(fieldPermissions)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private predicateToResult(predicate: Acl.Predicate | undefined): AuthorizationResult {
|
|
39
|
+
if (!predicate) {
|
|
40
|
+
return 'no'
|
|
41
|
+
}
|
|
42
|
+
if (predicate === true) {
|
|
43
|
+
return 'yes'
|
|
44
|
+
}
|
|
45
|
+
return 'maybe'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
isCustomPrimaryAllowed(entity: string): boolean {
|
|
50
|
+
return this.permissions?.[entity]?.operations?.customPrimary || false
|
|
51
|
+
}
|
|
8
52
|
}
|
package/src/acl/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { FieldMap } from '../EntityFieldsProvider'
|
|
|
3
3
|
import { PaginatedHasManyFieldProviderExtension } from './PaginatedHasManyFieldProvider'
|
|
4
4
|
import { capitalizeFirstLetter } from '../../utils'
|
|
5
5
|
import { PaginatedFieldConfigFactory } from '../../schema/PaginatedFieldConfigFactory'
|
|
6
|
+
import { aliasAwareResolver } from '../../schema'
|
|
6
7
|
|
|
7
8
|
export class PaginatedHasManyFieldProviderVisitor implements
|
|
8
9
|
Model.ColumnVisitor<FieldMap<PaginatedHasManyFieldProviderExtension>>,
|
|
@@ -47,6 +48,7 @@ export class PaginatedHasManyFieldProviderVisitor implements
|
|
|
47
48
|
extensions: {
|
|
48
49
|
relationName: relation.name,
|
|
49
50
|
},
|
|
51
|
+
resolve: aliasAwareResolver,
|
|
50
52
|
},
|
|
51
53
|
}
|
|
52
54
|
}
|
|
@@ -107,7 +107,7 @@ export class IntrospectionSchemaFactory {
|
|
|
107
107
|
},
|
|
108
108
|
})
|
|
109
109
|
return Object.values(getEntity(this.model, entityName).fields)
|
|
110
|
-
.filter(it => this.authorizator.
|
|
110
|
+
.filter(it => this.authorizator.getFieldPermissions(Acl.Operation.read, entityName, it.name) !== 'no')
|
|
111
111
|
.map((it): ContentSchema._Column | ContentSchema._Relation => ({
|
|
112
112
|
...additionalInfo[it.name],
|
|
113
113
|
name: it.name,
|
|
@@ -154,7 +154,7 @@ export class IntrospectionSchemaFactory {
|
|
|
154
154
|
|
|
155
155
|
public create(): ContentSchema._Schema {
|
|
156
156
|
const entities = Object.values(this.model.entities)
|
|
157
|
-
.filter(it => this.authorizator.
|
|
157
|
+
.filter(it => this.authorizator.getEntityPermission(Acl.Operation.read, it.name) !== 'no')
|
|
158
158
|
.map(entity => ({
|
|
159
159
|
name: entity.name,
|
|
160
160
|
customPrimaryAllowed: this.authorizator.isCustomPrimaryAllowed(entity.name),
|
package/src/mapper/Mapper.ts
CHANGED
|
@@ -120,11 +120,8 @@ export class Mapper {
|
|
|
120
120
|
const selector = this.selectBuilderFactory.create(augmentedBuilder, hydrator)
|
|
121
121
|
const filterWithPredicates = this.predicatesInjector.inject(entity, inputWithOrder.args.filter || {})
|
|
122
122
|
const inputWithPredicates = inputWithOrder.withArg('filter', filterWithPredicates)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
await selectPromise
|
|
126
|
-
|
|
127
|
-
return rows
|
|
123
|
+
selector.select(this, entity, inputWithPredicates, path, groupBy)
|
|
124
|
+
return await selector.execute(this.db)
|
|
128
125
|
}
|
|
129
126
|
|
|
130
127
|
public async count(entity: Model.Entity, filter: Input.OptionalWhere) {
|
|
@@ -30,7 +30,7 @@ export class OrderByBuilder {
|
|
|
30
30
|
): [SelectBuilder<SelectBuilder.Result>, Orderable] {
|
|
31
31
|
const entries = Object.entries(orderBy)
|
|
32
32
|
if (entries.length !== 1) {
|
|
33
|
-
const fields = entries.join(', ')
|
|
33
|
+
const fields = entries.map(it => it[0]).join(', ')
|
|
34
34
|
throw new UserError('Order by: only one field is expected in each item of order by clause, got: ' + fields)
|
|
35
35
|
}
|
|
36
36
|
if (orderBy._random || orderBy._randomSeeded) {
|
|
@@ -43,14 +43,14 @@ export class SelectBuilder {
|
|
|
43
43
|
return result
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
public
|
|
46
|
+
public select(
|
|
47
47
|
mapper: Mapper,
|
|
48
48
|
entity: Model.Entity,
|
|
49
49
|
input: ObjectNode<Input.ListQueryInput>,
|
|
50
50
|
path: Path,
|
|
51
51
|
groupBy?: string,
|
|
52
52
|
) {
|
|
53
|
-
|
|
53
|
+
this.selectInternal(mapper, entity, path, input)
|
|
54
54
|
const where = input.args.filter
|
|
55
55
|
if (where) {
|
|
56
56
|
this.qb = this.whereBuilder.build(this.qb, entity, path, where)
|
|
@@ -76,11 +76,9 @@ export class SelectBuilder {
|
|
|
76
76
|
}
|
|
77
77
|
this.qb = this.qb.limit(input.args.limit, input.args.offset)
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
await promise
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
private
|
|
81
|
+
private selectInternal(mapper: Mapper, entity: Model.Entity, path: Path, input: ObjectNode) {
|
|
84
82
|
if (!input.fields.find(it => it.name === entity.primary && it.alias === entity.primary)) {
|
|
85
83
|
input = input.withField(new FieldNode(entity.primary, entity.primary, {}))
|
|
86
84
|
}
|
|
@@ -142,8 +140,6 @@ export class SelectBuilder {
|
|
|
142
140
|
this.qb = this.qb.select([columnPath.back().getAlias(), columnName], columnPath.getAlias())
|
|
143
141
|
const rows = await this.rows
|
|
144
142
|
const columnAlias = columnPath.getAlias()
|
|
145
|
-
return rows
|
|
146
|
-
.map((it): Input.PrimaryValue => it[columnAlias] as Input.PrimaryValue)
|
|
147
|
-
.filter((val, index, all) => all.indexOf(val) === index)
|
|
143
|
+
return Array.from(new Set(rows.map((it): Input.PrimaryValue => it[columnAlias] as Input.PrimaryValue)))
|
|
148
144
|
}
|
|
149
145
|
}
|
|
@@ -378,12 +378,13 @@ export class MutationResolver {
|
|
|
378
378
|
if (errorResponse) {
|
|
379
379
|
return errorResponse
|
|
380
380
|
}
|
|
381
|
-
const primary = getInsertPrimary(result)
|
|
382
|
-
|
|
381
|
+
const primary = getInsertPrimary(result)
|
|
382
|
+
const byResolved = primary ? { [entity.primary]: primary } : input.by
|
|
383
|
+
if (!byResolved) {
|
|
383
384
|
throw new ImplementationException('MutationResolver::resolveUpsertInternal does not handle result properly')
|
|
384
385
|
}
|
|
385
386
|
|
|
386
|
-
const nodes = await this.resolveResultNodes(mapper, entity,
|
|
387
|
+
const nodes = await this.resolveResultNodes(mapper, entity, byResolved, queryAst)
|
|
387
388
|
return {
|
|
388
389
|
ok: true,
|
|
389
390
|
validation: {
|
|
@@ -53,14 +53,14 @@ export class EntityTypeProvider {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
public getEntity(entityName: string): GraphQLObjectType {
|
|
56
|
-
if (
|
|
56
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entityName) === 'no') {
|
|
57
57
|
throw new ImplementationException()
|
|
58
58
|
}
|
|
59
59
|
return this.entities(entityName)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
public getConnection(entityName: string): GraphQLObjectType {
|
|
63
|
-
if (
|
|
63
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entityName) === 'no') {
|
|
64
64
|
throw new ImplementationException()
|
|
65
65
|
}
|
|
66
66
|
return this.connections(entityName)
|
|
@@ -100,7 +100,7 @@ export class EntityTypeProvider {
|
|
|
100
100
|
|
|
101
101
|
const fields: { [field: string]: GraphQLFieldConfig<any, any> } = accessibleFields.reduce(
|
|
102
102
|
(result, fieldName) => {
|
|
103
|
-
const fieldTypeVisitor = new FieldTypeVisitor(this.columnTypeResolver, this)
|
|
103
|
+
const fieldTypeVisitor = new FieldTypeVisitor(this.columnTypeResolver, this, this.authorizator)
|
|
104
104
|
const type: GraphQLOutputType = acceptFieldVisitor(this.schema, entity, fieldName, fieldTypeVisitor)
|
|
105
105
|
|
|
106
106
|
const fieldArgsVisitor = new FieldArgsVisitor(this.whereTypeProvider, this.orderByTypeProvider)
|
|
@@ -8,10 +8,10 @@ export class FieldAccessVisitor implements Model.ColumnVisitor<boolean>, Model.R
|
|
|
8
8
|
) {}
|
|
9
9
|
|
|
10
10
|
visitColumn(entity: Model.Entity, column: Model.AnyColumn) {
|
|
11
|
-
return this.authorizator.
|
|
11
|
+
return this.authorizator.getFieldPermissions(this.operation, entity.name, column.name) !== 'no'
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
visitRelation({}, {}, targetEntity: Model.Entity) {
|
|
15
|
-
return this.authorizator.
|
|
15
|
+
return this.authorizator.getEntityPermission(this.operation, targetEntity.name) !== 'no'
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -69,7 +69,7 @@ export class MutationProvider {
|
|
|
69
69
|
if (entity.view) {
|
|
70
70
|
return undefined
|
|
71
71
|
}
|
|
72
|
-
if (
|
|
72
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.delete, entityName) === 'no') {
|
|
73
73
|
return undefined
|
|
74
74
|
}
|
|
75
75
|
const uniqueWhere = this.whereTypeProvider.getEntityUniqueWhereType(entityName)
|
|
@@ -180,7 +180,7 @@ export class MutationProvider {
|
|
|
180
180
|
errorMessage: { type: GraphQLString },
|
|
181
181
|
errors: { type: this.resultSchemaTypeProvider.errorListResultType },
|
|
182
182
|
}
|
|
183
|
-
if (this.authorizator.
|
|
183
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entityName) !== 'no') {
|
|
184
184
|
const nodeType = this.entityTypeProvider.getEntity(entityName)
|
|
185
185
|
fields.node = { type: nodeType, resolve: aliasAwareResolver }
|
|
186
186
|
}
|
|
@@ -19,7 +19,7 @@ export class QueryProvider {
|
|
|
19
19
|
) {}
|
|
20
20
|
|
|
21
21
|
public getQueries(entity: Model.Entity): { [fieldName: string]: GraphQLFieldConfig<any, Context, any> } {
|
|
22
|
-
if (
|
|
22
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entity.name) === 'no') {
|
|
23
23
|
return {}
|
|
24
24
|
}
|
|
25
25
|
return {
|
|
@@ -22,14 +22,14 @@ export class WhereTypeProvider {
|
|
|
22
22
|
) {}
|
|
23
23
|
|
|
24
24
|
public getEntityWhereType(entityName: string): GraphQLInputObjectType {
|
|
25
|
-
if (
|
|
25
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entityName) === 'no') {
|
|
26
26
|
throw new ImplementationException()
|
|
27
27
|
}
|
|
28
28
|
return this.whereSingleton(entityName)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
public getEntityUniqueWhereType(entityName: string): undefined | GraphQLInputObjectType {
|
|
32
|
-
if (
|
|
32
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entityName) === 'no') {
|
|
33
33
|
return undefined
|
|
34
34
|
}
|
|
35
35
|
return this.uniqueWhereSingleton(entityName)
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { GraphQLList, GraphQLNonNull, GraphQLOutputType } from 'graphql'
|
|
2
|
-
import { Model } from '@contember/schema'
|
|
2
|
+
import { Acl, Model } from '@contember/schema'
|
|
3
3
|
import { ColumnTypeResolver } from '../ColumnTypeResolver'
|
|
4
4
|
import { EntityTypeProvider } from '../EntityTypeProvider'
|
|
5
|
+
import { Authorizator } from '../../acl'
|
|
6
|
+
import { ImplementationException } from '../../exception'
|
|
5
7
|
|
|
6
8
|
export class FieldTypeVisitor implements Model.ColumnVisitor<GraphQLOutputType>, Model.RelationByGenericTypeVisitor<GraphQLOutputType> {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
this.entityTypeProvider = entityTypeProvider
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly columnTypeResolver: ColumnTypeResolver,
|
|
11
|
+
private readonly entityTypeProvider: EntityTypeProvider,
|
|
12
|
+
private readonly authorizator: Authorizator,
|
|
13
|
+
) {
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
public visitColumn(entity: Model.Entity, column: Model.AnyColumn): GraphQLOutputType {
|
|
16
17
|
const basicType = this.columnTypeResolver.getType(column)
|
|
17
|
-
|
|
18
|
+
const fieldPredicate = this.authorizator.getFieldPredicate(Acl.Operation.read, entity.name, column.name)
|
|
19
|
+
const idPredicate = this.authorizator.getFieldPredicate(Acl.Operation.read, entity.name, entity.primary)
|
|
20
|
+
if (!fieldPredicate || !idPredicate) {
|
|
21
|
+
throw new ImplementationException()
|
|
22
|
+
}
|
|
23
|
+
if (!column.nullable && (fieldPredicate === true || fieldPredicate === idPredicate)) {
|
|
24
|
+
return new GraphQLNonNull(basicType)
|
|
25
|
+
}
|
|
26
|
+
return basicType
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
public visitHasMany(entity: Model.Entity, relation: Model.Relation): GraphQLOutputType {
|
|
@@ -19,7 +19,7 @@ export class CreateEntityInputFieldVisitor implements
|
|
|
19
19
|
if (entity.primary === column.name && !this.authorizator.isCustomPrimaryAllowed(entity.name)) {
|
|
20
20
|
return undefined
|
|
21
21
|
}
|
|
22
|
-
if (
|
|
22
|
+
if (this.authorizator.getFieldPermissions(Acl.Operation.create, entity.name, column.name) === 'no') {
|
|
23
23
|
return undefined
|
|
24
24
|
}
|
|
25
25
|
const type = this.columnTypeResolver.getType(column)
|
|
@@ -65,9 +65,9 @@ export class CreateEntityRelationAllowedOperationsVisitor implements
|
|
|
65
65
|
): Input.CreateRelationOperation[] {
|
|
66
66
|
const result: Input.CreateRelationOperation[] = []
|
|
67
67
|
|
|
68
|
-
const canReadTargetEntity = this.authorizator.
|
|
69
|
-
const canCreateTargetEntity = this.authorizator.
|
|
70
|
-
const canCreateOwning = this.authorizator.
|
|
68
|
+
const canReadTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.read, targetEntity.name) !== 'no'
|
|
69
|
+
const canCreateTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.create, targetEntity.name) !== 'no'
|
|
70
|
+
const canCreateOwning = this.authorizator.getFieldPermissions(Acl.Operation.create, owningEntity.name, owningRelation.name) !== 'no'
|
|
71
71
|
|
|
72
72
|
if (canReadTargetEntity && canCreateOwning) {
|
|
73
73
|
result.push(Input.CreateRelationOperation.connect)
|
|
@@ -51,7 +51,7 @@ export class EntityInputProvider<Operation extends EntityInputType> {
|
|
|
51
51
|
throw new ImplementationException(`EntityInputProvider: Invalid operation ${this.operation}`)
|
|
52
52
|
}
|
|
53
53
|
})()
|
|
54
|
-
if (
|
|
54
|
+
if (this.authorizator.getEntityPermission(operation, entity.name) === 'no') {
|
|
55
55
|
return undefined
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -18,7 +18,7 @@ export class UpdateEntityInputFieldVisitor implements
|
|
|
18
18
|
if (entity.primary === column.name) {
|
|
19
19
|
return undefined
|
|
20
20
|
}
|
|
21
|
-
if (
|
|
21
|
+
if (this.authorizator.getFieldPermissions(Acl.Operation.update, entity.name, column.name) === 'no') {
|
|
22
22
|
return undefined
|
|
23
23
|
}
|
|
24
24
|
const type = this.columnTypeResolver.getType(column)
|
|
@@ -76,15 +76,11 @@ export class UpdateEntityRelationAllowedOperationsVisitor implements
|
|
|
76
76
|
): Input.UpdateRelationOperation[] {
|
|
77
77
|
const result: Input.UpdateRelationOperation[] = []
|
|
78
78
|
|
|
79
|
-
const canReadTargetEntity = this.authorizator.
|
|
80
|
-
const canCreateTargetEntity = this.authorizator.
|
|
81
|
-
const canUpdateTargetEntity = this.authorizator.
|
|
82
|
-
const canDeleteTargetEntity = this.authorizator.
|
|
83
|
-
const canUpdateOwningRelation = this.authorizator.
|
|
84
|
-
Acl.Operation.update,
|
|
85
|
-
owningEntity.name,
|
|
86
|
-
owningRelation.name,
|
|
87
|
-
)
|
|
79
|
+
const canReadTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.read, targetEntity.name) !== 'no'
|
|
80
|
+
const canCreateTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.create, targetEntity.name) !== 'no'
|
|
81
|
+
const canUpdateTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.update, targetEntity.name) !== 'no'
|
|
82
|
+
const canDeleteTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.delete, targetEntity.name) !== 'no'
|
|
83
|
+
const canUpdateOwningRelation = this.authorizator.getFieldPermissions(Acl.Operation.update, owningEntity.name, owningRelation.name) !== 'no'
|
|
88
84
|
|
|
89
85
|
if (canReadTargetEntity && canUpdateOwningRelation) {
|
|
90
86
|
result.push(Input.UpdateRelationOperation.connect)
|
|
@@ -174,7 +174,7 @@ export class UpdateEntityRelationInputFieldVisitor implements
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
private createWhereInput(targetEntity: Model.Entity): GraphQLInputFieldConfig | undefined {
|
|
177
|
-
if (
|
|
177
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, targetEntity.name) === 'no') {
|
|
178
178
|
return undefined
|
|
179
179
|
}
|
|
180
180
|
const uniqueWhere = this.whereTypeBuilder.getEntityUniqueWhereType(targetEntity.name)
|
|
@@ -416,4 +416,93 @@ test('Posts with paginated comments without count (one has many)', async () => {
|
|
|
416
416
|
})
|
|
417
417
|
})
|
|
418
418
|
|
|
419
|
+
|
|
420
|
+
test('Posts with paginated comments (one has many)', async () => {
|
|
421
|
+
await execute({
|
|
422
|
+
schema: new SchemaBuilder()
|
|
423
|
+
.entity('Post', entity => entity.oneHasMany('comments', relation => relation.target('Comment').ownedBy('post')))
|
|
424
|
+
.entity('Comment', entity => entity.column('content').column('deletedAt', c => c.type(Model.ColumnType.DateTime)))
|
|
425
|
+
.buildSchema(),
|
|
426
|
+
query: GQL`
|
|
427
|
+
query {
|
|
428
|
+
listPost {
|
|
429
|
+
id
|
|
430
|
+
comments: paginateComments(filter: {deletedAt: {isNull: true}}, first: 1) {
|
|
431
|
+
pages: pageInfo {
|
|
432
|
+
count: totalCount
|
|
433
|
+
}
|
|
434
|
+
comments: edges {
|
|
435
|
+
comment: node {
|
|
436
|
+
content
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
`,
|
|
443
|
+
executes: [
|
|
444
|
+
{
|
|
445
|
+
sql: SQL`select
|
|
446
|
+
"root_"."id" as "root_id",
|
|
447
|
+
"root_"."id" as "root_id"
|
|
448
|
+
from "public"."post" as "root_"`,
|
|
449
|
+
response: {
|
|
450
|
+
rows: [{ root_id: testUuid(1) }],
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
sql: SQL`select
|
|
455
|
+
count(*) as "row_count", "root_"."post_id"
|
|
456
|
+
from "public"."comment" as "root_"
|
|
457
|
+
where "root_"."deleted_at" is null and "root_"."post_id" in (?)
|
|
458
|
+
group by "root_"."post_id"`,
|
|
459
|
+
parameters: [testUuid(1)],
|
|
460
|
+
response: {
|
|
461
|
+
rows: [
|
|
462
|
+
{ post_id: testUuid(1), row_count: 5 },
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
sql: SQL`with "data" as
|
|
468
|
+
(select
|
|
469
|
+
"root_"."post_id" as "__grouping_key",
|
|
470
|
+
"root_"."content" as "root_content",
|
|
471
|
+
"root_"."id" as "root_id",
|
|
472
|
+
row_number() over(partition by "root_"."post_id" order by "root_"."id" asc) as "rowNumber_"
|
|
473
|
+
from "public"."comment" as "root_"
|
|
474
|
+
where "root_"."deleted_at" is null
|
|
475
|
+
and "root_"."post_id" in (?)
|
|
476
|
+
order by "root_"."id" asc)
|
|
477
|
+
select "data".* from "data" where "data"."rowNumber_" <= ?`,
|
|
478
|
+
parameters: [testUuid(1), 1],
|
|
479
|
+
response: {
|
|
480
|
+
rows: [
|
|
481
|
+
{ __grouping_key: testUuid(1), root_id: testUuid(3), root_content: 'Lorem ipsum' },
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
return: {
|
|
487
|
+
data: {
|
|
488
|
+
listPost: [
|
|
489
|
+
{
|
|
490
|
+
id: testUuid(1),
|
|
491
|
+
comments: {
|
|
492
|
+
pages: {
|
|
493
|
+
count: 5,
|
|
494
|
+
},
|
|
495
|
+
comments: [
|
|
496
|
+
{
|
|
497
|
+
comment: { content: 'Lorem ipsum' },
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
})
|
|
506
|
+
})
|
|
507
|
+
|
|
419
508
|
test.run()
|
|
@@ -98,8 +98,54 @@ test('upsert author (exists)', async () => {
|
|
|
98
98
|
response: { rowCount: 1 },
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
|
-
sql: SQL`select "root_"."id" as "root_id" from "public"."author" as "root_" where "root_"."
|
|
102
|
-
parameters: [
|
|
101
|
+
sql: SQL`select "root_"."id" as "root_id" from "public"."author" as "root_" where "root_"."slug" = ?`,
|
|
102
|
+
parameters: ['john-doe'],
|
|
103
|
+
response: { rows: [{ root_id: testUuid(2) }] },
|
|
104
|
+
},
|
|
105
|
+
]),
|
|
106
|
+
],
|
|
107
|
+
return: {
|
|
108
|
+
data: {
|
|
109
|
+
upsertAuthor: {
|
|
110
|
+
ok: true,
|
|
111
|
+
node: {
|
|
112
|
+
id: testUuid(2),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('upsert author (exists) with noop update)', async () => {
|
|
121
|
+
await execute({
|
|
122
|
+
schema: new SchemaBuilder()
|
|
123
|
+
.entity('Author', entity =>
|
|
124
|
+
entity
|
|
125
|
+
.column('name', c => c.type(Model.ColumnType.String))
|
|
126
|
+
.column('slug', c => c.type(Model.ColumnType.String).unique()),
|
|
127
|
+
)
|
|
128
|
+
.buildSchema(),
|
|
129
|
+
query: GQL`
|
|
130
|
+
mutation {
|
|
131
|
+
upsertAuthor(by: {slug: "john-doe"}, update: {}, create: {slug: "john-doe", name: "John Doe"}) {
|
|
132
|
+
ok
|
|
133
|
+
node {
|
|
134
|
+
id
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}`,
|
|
138
|
+
executes: [
|
|
139
|
+
...sqlTransaction([
|
|
140
|
+
{
|
|
141
|
+
sql: SQL`select "root_"."id"
|
|
142
|
+
from "public"."author" as "root_" where "root_"."slug" = ?`,
|
|
143
|
+
parameters: ['john-doe'],
|
|
144
|
+
response: { rows: [{ id: testUuid(2) }] },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
sql: SQL`select "root_"."id" as "root_id" from "public"."author" as "root_" where "root_"."slug" = ?`,
|
|
148
|
+
parameters: ['john-doe'],
|
|
103
149
|
response: { rows: [{ root_id: testUuid(2) }] },
|
|
104
150
|
},
|
|
105
151
|
]),
|
|
@@ -117,3 +163,4 @@ test('upsert author (exists)', async () => {
|
|
|
117
163
|
})
|
|
118
164
|
})
|
|
119
165
|
test.run()
|
|
166
|
+
|
|
@@ -3,7 +3,7 @@ import { Acl, Model } from '@contember/schema'
|
|
|
3
3
|
import * as path from 'path'
|
|
4
4
|
|
|
5
5
|
import { AllowAllPermissionFactory, SchemaBuilder, SchemaDefinition } from '@contember/schema-definition'
|
|
6
|
-
import { GraphQlSchemaBuilderFactory,
|
|
6
|
+
import { GraphQlSchemaBuilderFactory, Authorizator } from '../../../../src'
|
|
7
7
|
import * as model from './model'
|
|
8
8
|
import { promises as fs } from 'fs'
|
|
9
9
|
import * as assert from 'uvu/assert'
|
|
@@ -23,7 +23,7 @@ const testSchema = async (test: Test) => {
|
|
|
23
23
|
const schema = schemaResult instanceof SchemaBuilder ? schemaResult.buildSchema() : schemaResult
|
|
24
24
|
const schemaWithAcl = { ...schema, acl: { roles: {}, variables: {} } }
|
|
25
25
|
const permissions = test.permissions(schemaWithAcl)
|
|
26
|
-
const authorizator = new
|
|
26
|
+
const authorizator = new Authorizator(permissions)
|
|
27
27
|
const graphQlSchemaBuilder = schemaFactory.create(schemaWithAcl, authorizator)
|
|
28
28
|
const graphQlSchema = graphQlSchemaBuilder.build()
|
|
29
29
|
|
|
@@ -110,6 +110,61 @@ graphqlSchemaBuilderTest('restricted access to fields by permissions', async ()
|
|
|
110
110
|
graphQlSchemaFile: 'schema-acl.gql',
|
|
111
111
|
})
|
|
112
112
|
})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
graphqlSchemaBuilderTest('conditionally restricted read of some fields', async () => {
|
|
116
|
+
await testSchema({
|
|
117
|
+
schema: builder =>
|
|
118
|
+
builder.entity('Test', e =>
|
|
119
|
+
e
|
|
120
|
+
.column('a', c => c.type(Model.ColumnType.String).notNull()),
|
|
121
|
+
),
|
|
122
|
+
permissions: () => ({
|
|
123
|
+
Test: {
|
|
124
|
+
predicates: {
|
|
125
|
+
testPredicate: {
|
|
126
|
+
a: { eq: 'Foo' },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
operations: {
|
|
130
|
+
read: {
|
|
131
|
+
id: true,
|
|
132
|
+
a: 'testPredicate',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
graphQlSchemaFile: 'schema-acl-predicate.gql',
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
graphqlSchemaBuilderTest('conditionally restricted read of whole row', async () => {
|
|
143
|
+
await testSchema({
|
|
144
|
+
schema: builder =>
|
|
145
|
+
builder.entity('Test', e =>
|
|
146
|
+
e
|
|
147
|
+
.column('a', c => c.type(Model.ColumnType.String).notNull()),
|
|
148
|
+
),
|
|
149
|
+
permissions: () => ({
|
|
150
|
+
Test: {
|
|
151
|
+
predicates: {
|
|
152
|
+
testPredicate: {
|
|
153
|
+
a: { eq: 'Foo' },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
operations: {
|
|
157
|
+
read: {
|
|
158
|
+
id: 'testPredicate',
|
|
159
|
+
a: 'testPredicate',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
graphQlSchemaFile: 'schema-acl-predicate2.gql',
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
113
168
|
const oneHasManySchema = (builder: SchemaBuilder) =>
|
|
114
169
|
builder.entity('Root', e =>
|
|
115
170
|
e
|