Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.brennaswitzer.cookbook.config.AWSProperties;
import com.brennaswitzer.cookbook.config.AppProperties;
import com.brennaswitzer.cookbook.config.CalendarProperties;
import com.brennaswitzer.cookbook.config.GraphqlProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
Expand All @@ -11,7 +12,8 @@
@EnableConfigurationProperties({
AppProperties.class,
AWSProperties.class,
CalendarProperties.class })
CalendarProperties.class,
GraphqlProperties.class })
public class CookbookApplication {

public static void main(String[] args) {
Expand Down
19 changes: 17 additions & 2 deletions src/main/java/com/brennaswitzer/cookbook/config/GraphQLConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.brennaswitzer.cookbook.graphql.support.CachingPreparsedDocumentProvider;
import com.brennaswitzer.cookbook.graphql.support.OffsetConnectionCursorCoercing;
import com.brennaswitzer.cookbook.util.ValueUtils;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Scheduler;
import graphql.ExecutionResult;
Expand Down Expand Up @@ -85,7 +86,8 @@ PreparsedDocumentProvider preparsedDocumentProvider() {
GraphQlSourceBuilderCustomizer sourceBuilderCustomizer(
Collection<GraphQLScalarType> scalars,
TransactionTemplate mutationTmpl,
DataFetcherExceptionHandler exceptionHandler) {
DataFetcherExceptionHandler exceptionHandler,
GraphqlProperties graphqlProperties) {

// The warning is for IntelliJ's over-aggro application of @NotNull to
// places where library authors omitted it. In this case, Spring
Expand Down Expand Up @@ -113,7 +115,20 @@ GraphQlSourceBuilderCustomizer sourceBuilderCustomizer(
.mutationExecutionStrategy(mutationStrat)
.preparsedDocumentProvider(preparsedDocumentProvider()))
.configureRuntimeWiring(wiring -> scalars.forEach(wiring::scalar))
.inspectSchemaMappings(report -> log.info("{}", report));
.inspectSchemaMappings(report -> {
if (ValueUtils.hasValue(report.unmappedFields())
|| ValueUtils.hasValue(report.unmappedRegistrations())
|| ValueUtils.hasValue(report.unmappedArguments())
|| ValueUtils.hasValue(report.skippedTypes())) {
if (graphqlProperties.isFailOnUnmapped()) {
log.warn("{}", report);
throw new IllegalStateException("GraphQL schema mapping is incomplete");
}
log.warn("{}\n\n\n¡¡UNMAPPED SCHEMA OBJECTS EXIST!!\n\n", report);
} else {
log.debug("{}", report);
}
});
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.brennaswitzer.cookbook.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app.graphql")
@Getter
@Setter
public class GraphqlProperties {

private boolean failOnUnmapped;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.brennaswitzer.cookbook.graphql.resolvers;

import com.brennaswitzer.cookbook.domain.Owned;
import com.brennaswitzer.cookbook.domain.User;
import com.brennaswitzer.cookbook.security.CurrentUser;
import com.brennaswitzer.cookbook.security.UserPrincipal;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;

@Controller
public class OwnedResolver {

@SchemaMapping
public User ownedBy(Owned owned,
@CurrentUser UserPrincipal userPrincipal) {
if (mine(owned, userPrincipal)) return null;
return owned.getOwner();
}

@SchemaMapping
public boolean mine(Owned owned,
@CurrentUser UserPrincipal userPrincipal) {
return userPrincipal != null
&& owned.isOwner(userPrincipal);
}

@SchemaMapping
public boolean notMine(Owned owned,
@CurrentUser UserPrincipal userPrincipal) {
return !mine(owned, userPrincipal);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ public class UserResolver {
@Autowired
private AssembleUserPreferences assembleUserPreferences;

@SchemaMapping
public boolean me(User user,
@CurrentUser UserPrincipal principal) {
return user.getId().equals(principal.getId());
}

@SchemaMapping
public boolean notMe(User user,
@CurrentUser UserPrincipal principal) {
return !me(user, principal);
}

@SchemaMapping
public List<String> roles(User user,
@CurrentUser UserPrincipal principal) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,5 @@ app:
- https://next.gobrennas.com/post-oauth2/redirect
calendar:
validate: false
graphql:
failOnUnmapped: false
3 changes: 3 additions & 0 deletions src/main/resources/graphqls/library.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ enum ChronoUnit {
type Recipe implements Node & Owned & Ingredient {
id: ID!
owner: User!
ownedBy: User
mine: Boolean!
notMine: Boolean!
name: String!
externalUrl: String
directions: String
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/graphqls/planner.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ type Plan implements Node & Owned & AccessControlled & CorePlanItem {
"""The plan's owner
"""
owner: User!
ownedBy: User
mine: Boolean!
notMine: Boolean!
notes: String
share: ShareInfo!
bucketCount: NonNegativeInt!
Expand Down
22 changes: 20 additions & 2 deletions src/main/resources/graphqls/profile.graphqls
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
interface Owned {
"""The user who owns this object."""
owner: User!
"""
Another user who owns this object. Always null if the current user is the
owner, always the same as `owner` otherwise.
"""
ownedBy: User
"""Is the owner of this object me (the current user)?"""
mine: Boolean!
"""Is the owner of this object not me (the current user)?"""
notMine: Boolean!
}

interface AccessControlled implements Owned {
owner: User!
ownedBy: User
mine: Boolean!
notMine: Boolean!
"""The object's ACL, which includes its owner and any grants of access.
"""
acl: Acl!
Expand All @@ -13,8 +25,12 @@ interface AccessControlled implements Owned {

type Acl implements Owned {
owner: User!
"""Users granted access, by the owner. This is conceptually map, so a given
user (the key) uniquely identifies their access level (the value).
ownedBy: User
mine: Boolean!
notMine: Boolean!
"""Users granted access by the owner or an administrator. Conceptually,
this is a map, so a given user (the key) uniquely identifies their access
level (the value).
"""
grants: [AccessControlEntry!]!
}
Expand Down Expand Up @@ -55,6 +71,8 @@ type User implements Node {
are used, if they exists. If not, the static default is returned.
"""
preferences(deviceKey: String): [UserPreference!]!
me: Boolean!
notMe: Boolean!
}

type UserPreference {
Expand Down
2 changes: 2 additions & 0 deletions src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ app:
token-secrets:
- id: test
secret: TcJRn7dQqCbUfI5CV8F3DZLVw5I098k74jiKfw03OK74pzGeP6iP+jO/Ti7HmEV0fgbl+GzhMmPFZELBDeUZsg==
graphql:
failOnUnmapped: true

logging:
level:
Expand Down