@contember/engine-content-api 1.0.0-rc.9 → 1.0.2

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.
Files changed (105) hide show
  1. package/dist/src/acl/Authorizator.d.ts +8 -3
  2. package/dist/src/acl/Authorizator.d.ts.map +1 -1
  3. package/dist/src/acl/Authorizator.js +46 -0
  4. package/dist/src/acl/Authorizator.js.map +1 -1
  5. package/dist/src/acl/index.d.ts +0 -1
  6. package/dist/src/acl/index.d.ts.map +1 -1
  7. package/dist/src/acl/index.js +0 -1
  8. package/dist/src/acl/index.js.map +1 -1
  9. package/dist/src/extensions/paginatedHasMany/PaginatedHasManyFieldProviderVisitor.d.ts.map +1 -1
  10. package/dist/src/extensions/paginatedHasMany/PaginatedHasManyFieldProviderVisitor.js +2 -0
  11. package/dist/src/extensions/paginatedHasMany/PaginatedHasManyFieldProviderVisitor.js.map +1 -1
  12. package/dist/src/introspection/IntrospectionSchemaFactory.js +2 -2
  13. package/dist/src/introspection/IntrospectionSchemaFactory.js.map +1 -1
  14. package/dist/src/mapper/Mapper.d.ts.map +1 -1
  15. package/dist/src/mapper/Mapper.js +2 -4
  16. package/dist/src/mapper/Mapper.js.map +1 -1
  17. package/dist/src/mapper/select/ConditionBuilder.d.ts +1 -1
  18. package/dist/src/mapper/select/ConditionBuilder.d.ts.map +1 -1
  19. package/dist/src/mapper/select/ConditionBuilder.js +5 -5
  20. package/dist/src/mapper/select/ConditionBuilder.js.map +1 -1
  21. package/dist/src/mapper/select/OrderByBuilder.js +1 -1
  22. package/dist/src/mapper/select/OrderByBuilder.js.map +1 -1
  23. package/dist/src/mapper/select/SelectBuilder.d.ts +1 -1
  24. package/dist/src/mapper/select/SelectBuilder.d.ts.map +1 -1
  25. package/dist/src/mapper/select/SelectBuilder.js +4 -7
  26. package/dist/src/mapper/select/SelectBuilder.js.map +1 -1
  27. package/dist/src/mapper/select/WhereBuilder.d.ts.map +1 -1
  28. package/dist/src/mapper/select/WhereBuilder.js +4 -3
  29. package/dist/src/mapper/select/WhereBuilder.js.map +1 -1
  30. package/dist/src/resolvers/MutationResolver.d.ts.map +1 -1
  31. package/dist/src/resolvers/MutationResolver.js +4 -3
  32. package/dist/src/resolvers/MutationResolver.js.map +1 -1
  33. package/dist/src/schema/EntityTypeProvider.js +3 -3
  34. package/dist/src/schema/EntityTypeProvider.js.map +1 -1
  35. package/dist/src/schema/FieldAccessVisitor.js +2 -2
  36. package/dist/src/schema/FieldAccessVisitor.js.map +1 -1
  37. package/dist/src/schema/MutationProvider.js +2 -2
  38. package/dist/src/schema/MutationProvider.js.map +1 -1
  39. package/dist/src/schema/QueryProvider.js +1 -1
  40. package/dist/src/schema/QueryProvider.js.map +1 -1
  41. package/dist/src/schema/WhereTypeProvider.js +2 -2
  42. package/dist/src/schema/WhereTypeProvider.js.map +1 -1
  43. package/dist/src/schema/entities/FieldTypeVisitor.d.ts +5 -3
  44. package/dist/src/schema/entities/FieldTypeVisitor.d.ts.map +1 -1
  45. package/dist/src/schema/entities/FieldTypeVisitor.js +13 -2
  46. package/dist/src/schema/entities/FieldTypeVisitor.js.map +1 -1
  47. package/dist/src/schema/mutations/CreateEntityInputFieldVisitor.js +1 -1
  48. package/dist/src/schema/mutations/CreateEntityInputFieldVisitor.js.map +1 -1
  49. package/dist/src/schema/mutations/CreateEntityRelationAllowedOperationsVisitor.js +3 -3
  50. package/dist/src/schema/mutations/CreateEntityRelationAllowedOperationsVisitor.js.map +1 -1
  51. package/dist/src/schema/mutations/EntityInputProvider.js +1 -1
  52. package/dist/src/schema/mutations/EntityInputProvider.js.map +1 -1
  53. package/dist/src/schema/mutations/UpdateEntityInputFieldVisitor.js +1 -1
  54. package/dist/src/schema/mutations/UpdateEntityInputFieldVisitor.js.map +1 -1
  55. package/dist/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.d.ts.map +1 -1
  56. package/dist/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.js +5 -5
  57. package/dist/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.js.map +1 -1
  58. package/dist/src/schema/mutations/UpdateEntityRelationInputFieldVisitor.js +1 -1
  59. package/dist/src/schema/mutations/UpdateEntityRelationInputFieldVisitor.js.map +1 -1
  60. package/dist/src/tsconfig.tsbuildinfo +1 -1
  61. package/dist/tests/cases/integration/graphQlRequests/query/relations/oneHasMany.test.js +87 -0
  62. package/dist/tests/cases/integration/graphQlRequests/query/relations/oneHasMany.test.js.map +1 -1
  63. package/dist/tests/cases/integration/graphQlRequests/upsert/basicUpsert.test.js +45 -2
  64. package/dist/tests/cases/integration/graphQlRequests/upsert/basicUpsert.test.js.map +1 -1
  65. package/dist/tests/cases/integration/graphQlSchema/graphQlSchemaBuilder.test.js +45 -1
  66. package/dist/tests/cases/integration/graphQlSchema/graphQlSchemaBuilder.test.js.map +1 -1
  67. package/dist/tests/cases/integration/graphQlSchema/schema-acl-predicate.gql +110 -0
  68. package/dist/tests/cases/integration/graphQlSchema/schema-acl-predicate2.gql +110 -0
  69. package/dist/tests/src/test.js +1 -1
  70. package/dist/tests/src/test.js.map +1 -1
  71. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +10 -10
  73. package/src/acl/Authorizator.ts +48 -4
  74. package/src/acl/index.ts +0 -1
  75. package/src/extensions/paginatedHasMany/PaginatedHasManyFieldProviderVisitor.ts +2 -0
  76. package/src/introspection/IntrospectionSchemaFactory.ts +2 -2
  77. package/src/mapper/Mapper.ts +2 -5
  78. package/src/mapper/select/ConditionBuilder.ts +5 -4
  79. package/src/mapper/select/OrderByBuilder.ts +1 -1
  80. package/src/mapper/select/SelectBuilder.ts +4 -8
  81. package/src/mapper/select/WhereBuilder.ts +5 -2
  82. package/src/resolvers/MutationResolver.ts +4 -3
  83. package/src/schema/EntityTypeProvider.ts +3 -3
  84. package/src/schema/FieldAccessVisitor.ts +2 -2
  85. package/src/schema/MutationProvider.ts +2 -2
  86. package/src/schema/QueryProvider.ts +1 -1
  87. package/src/schema/WhereTypeProvider.ts +2 -2
  88. package/src/schema/entities/FieldTypeVisitor.ts +17 -8
  89. package/src/schema/mutations/CreateEntityInputFieldVisitor.ts +1 -1
  90. package/src/schema/mutations/CreateEntityRelationAllowedOperationsVisitor.ts +3 -3
  91. package/src/schema/mutations/EntityInputProvider.ts +1 -1
  92. package/src/schema/mutations/UpdateEntityInputFieldVisitor.ts +1 -1
  93. package/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.ts +5 -9
  94. package/src/schema/mutations/UpdateEntityRelationInputFieldVisitor.ts +1 -1
  95. package/tests/cases/integration/graphQlRequests/query/relations/oneHasMany.test.ts +89 -0
  96. package/tests/cases/integration/graphQlRequests/upsert/basicUpsert.test.ts +49 -2
  97. package/tests/cases/integration/graphQlSchema/graphQlSchemaBuilder.test.ts +57 -2
  98. package/tests/cases/integration/graphQlSchema/schema-acl-predicate.gql +110 -0
  99. package/tests/cases/integration/graphQlSchema/schema-acl-predicate2.gql +110 -0
  100. package/tests/src/test.ts +2 -2
  101. package/dist/src/acl/StaticAuthorizator.d.ts +0 -9
  102. package/dist/src/acl/StaticAuthorizator.d.ts.map +0 -1
  103. package/dist/src/acl/StaticAuthorizator.js +0 -32
  104. package/dist/src/acl/StaticAuthorizator.js.map +0 -1
  105. 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.0-rc.9",
3
+ "version": "1.0.2",
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.0-rc.9",
14
- "@contember/dic": "^1.0.0-rc.9",
15
- "@contember/engine-common": "^1.0.0-rc.9",
16
- "@contember/graphql-utils": "^1.0.0-rc.9",
17
- "@contember/schema": "^1.0.0-rc.9",
18
- "@contember/schema-utils": "^1.0.0-rc.9",
13
+ "@contember/database": "^1.0.2",
14
+ "@contember/dic": "^1.0.2",
15
+ "@contember/engine-common": "^1.0.2",
16
+ "@contember/graphql-utils": "^1.0.2",
17
+ "@contember/schema": "^1.0.2",
18
+ "@contember/schema-utils": "^1.0.2",
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.0-rc.9",
24
- "@contember/engine-api-tester": "^1.0.0-rc.9",
25
- "@contember/schema-definition": "^1.0.0-rc.9",
23
+ "@contember/database-tester": "^1.0.2",
24
+ "@contember/engine-api-tester": "^1.0.2",
25
+ "@contember/schema-definition": "^1.0.2",
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",
@@ -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
- isCustomPrimaryAllowed(entity: string): boolean
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,5 @@ export * from './PermissionFactory'
3
3
  export * from './PermissionsByIdentityFactory'
4
4
  export * from './PredicateFactory'
5
5
  export * from './PredicatesInjector'
6
- export * from './StaticAuthorizator'
7
6
  export * from './VariableInjector'
8
7
  export * from './VariableUtils'
@@ -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.isAllowed(Acl.Operation.read, entityName, it.name))
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.isAllowed(Acl.Operation.read, it.name))
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),
@@ -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
- const selectPromise = selector.select(this, entity, inputWithPredicates, path, groupBy)
124
- const rows = await selector.execute(this.db)
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) {
@@ -7,6 +7,7 @@ export class ConditionBuilder {
7
7
  builder: SqlConditionBuilder,
8
8
  tableName: string,
9
9
  columnName: string,
10
+ columnType: string,
10
11
  condition: Input.Condition<any>,
11
12
  ): SqlConditionBuilder {
12
13
  const keys = Object.keys(condition) as (keyof Required<Input.Condition<any>>)[]
@@ -27,13 +28,13 @@ export class ConditionBuilder {
27
28
  param: Exclude<Input.Condition<any>[K], undefined>,
28
29
  ) => SqlConditionBuilder
29
30
  } = {
30
- and: (builder, expressions) => builder.and(builder2 => expressions.reduce((builder3, expr) => this.build(builder3, tableName, columnName, expr), builder2)),
31
- or: (builder, expressions) => builder.or(builder2 => expressions.reduce((builder3, expr) => this.build(builder3, tableName, columnName, expr), builder2)),
32
- not: (builder, expression) => builder.not(builder2 => this.build(builder2, tableName, columnName, expression)),
31
+ and: (builder, expressions) => builder.and(builder2 => expressions.reduce((builder3, expr) => this.build(builder3, tableName, columnName, columnType, expr), builder2)),
32
+ or: (builder, expressions) => builder.or(builder2 => expressions.reduce((builder3, expr) => this.build(builder3, tableName, columnName, columnType, expr), builder2)),
33
+ not: (builder, expression) => builder.not(builder2 => this.build(builder2, tableName, columnName, columnType, expression)),
33
34
  eq: (builder, value) => builder.compare(columnIdentifier, Operator.eq, value),
34
35
  notEq: (builder, value) => builder.compare(columnIdentifier, Operator.notEq, value),
35
36
  isNull: (builder, value) => value ? builder.isNull(columnIdentifier) : builder.not(clause => clause.isNull(columnIdentifier)),
36
- in: (builder, values) => builder.in(columnIdentifier, values),
37
+ in: (builder, values) => builder.in(columnIdentifier, values, columnType),
37
38
  notIn: (builder, values) => builder.not(builder2 => builder2.in(columnIdentifier, values)),
38
39
  lt: (builder, value) => builder.compare(columnIdentifier, Operator.lt, value),
39
40
  lte: (builder, value) => builder.compare(columnIdentifier, Operator.lte, value),
@@ -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 async select(
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
- const promise = this.selectInternal(mapper, entity, path, input)
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 async selectInternal(mapper: Mapper, entity: Model.Entity, path: Path, input: ObjectNode) {
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
  }
@@ -101,6 +101,7 @@ export class WhereBuilder {
101
101
  conditionBuilder,
102
102
  tableName,
103
103
  relation.joiningColumn.columnName,
104
+ (targetEntity.fields[targetEntity.primary] as Model.AnyColumn).columnType,
104
105
  primaryCondition,
105
106
  )
106
107
  }
@@ -114,7 +115,7 @@ export class WhereBuilder {
114
115
  conditionBuilder = acceptFieldVisitor<SqlConditionBuilder>(this.schema, entity, fieldName, {
115
116
  visitColumn: (entity, column) => {
116
117
  const subWhere: Input.Condition<Input.ColumnValue> = where[column.name] as Input.Condition<Input.ColumnValue>
117
- return this.conditionBuilder.build(conditionBuilder, tableName, column.columnName, subWhere)
118
+ return this.conditionBuilder.build(conditionBuilder, tableName, column.columnName, column.columnType, subWhere)
118
119
  },
119
120
  visitOneHasOneInverse: joinedWhere,
120
121
  visitOneHasOneOwning: joinedWhere,
@@ -198,8 +199,10 @@ export class WhereBuilder {
198
199
  .select(['junction_', fromColumn])
199
200
  const primaryCondition = this.transformWhereToPrimaryCondition(relationWhere, targetEntity.primary)
200
201
  if (primaryCondition !== null) {
202
+ const columnType = (targetEntity.fields[targetEntity.primary] as Model.AnyColumn).columnType
203
+
201
204
  return augmentedBuilder.where(condition =>
202
- this.conditionBuilder.build(condition, 'junction_', toColumn, primaryCondition),
205
+ this.conditionBuilder.build(condition, 'junction_', toColumn, columnType, primaryCondition),
203
206
  )
204
207
  }
205
208
 
@@ -378,12 +378,13 @@ export class MutationResolver {
378
378
  if (errorResponse) {
379
379
  return errorResponse
380
380
  }
381
- const primary = getInsertPrimary(result) || getUpdatePrimary(result)
382
- if (!primary) {
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, { [entity.primary]: primary }, queryAst)
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 (!this.authorizator.isAllowed(Acl.Operation.read, entityName)) {
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 (!this.authorizator.isAllowed(Acl.Operation.read, entityName)) {
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.isAllowed(this.operation, entity.name, column.name)
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.isAllowed(this.operation, targetEntity.name)
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 (!this.authorizator.isAllowed(Acl.Operation.delete, entityName)) {
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.isAllowed(Acl.Operation.read, entityName)) {
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 (!this.authorizator.isAllowed(Acl.Operation.read, entity.name)) {
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 (!this.authorizator.isAllowed(Acl.Operation.read, entityName)) {
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 (!this.authorizator.isAllowed(Acl.Operation.read, entityName)) {
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
- private columnTypeResolver: ColumnTypeResolver
8
- private entityTypeProvider: EntityTypeProvider
9
-
10
- constructor(columnTypeResolver: ColumnTypeResolver, entityTypeProvider: EntityTypeProvider) {
11
- this.columnTypeResolver = columnTypeResolver
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
- return column.nullable ? basicType : new GraphQLNonNull(basicType)
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 (!this.authorizator.isAllowed(Acl.Operation.create, entity.name, column.name)) {
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.isAllowed(Acl.Operation.read, targetEntity.name)
69
- const canCreateTargetEntity = this.authorizator.isAllowed(Acl.Operation.create, targetEntity.name)
70
- const canCreateOwning = this.authorizator.isAllowed(Acl.Operation.create, owningEntity.name, owningRelation.name)
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 (!this.authorizator.isAllowed(operation, entity.name)) {
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 (!this.authorizator.isAllowed(Acl.Operation.update, entity.name, column.name)) {
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.isAllowed(Acl.Operation.read, targetEntity.name)
80
- const canCreateTargetEntity = this.authorizator.isAllowed(Acl.Operation.create, targetEntity.name)
81
- const canUpdateTargetEntity = this.authorizator.isAllowed(Acl.Operation.update, targetEntity.name)
82
- const canDeleteTargetEntity = this.authorizator.isAllowed(Acl.Operation.delete, targetEntity.name)
83
- const canUpdateOwningRelation = this.authorizator.isAllowed(
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 (!this.authorizator.isAllowed(Acl.Operation.read, targetEntity.name)) {
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_"."id" = ?`,
102
- parameters: [testUuid(2)],
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
+