-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Per-entity CRUD filters for update/delete operations #502
.. read filter for updates
- Loading branch information
Showing
10 changed files
with
335 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_ReadFilterIT.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
package io.agrest.cayenne; | ||
|
||
import io.agrest.Ag; | ||
import io.agrest.DataResponse; | ||
import io.agrest.EntityUpdate; | ||
import io.agrest.access.ReadFilter; | ||
import io.agrest.cayenne.cayenne.main.E2; | ||
import io.agrest.cayenne.cayenne.main.E3; | ||
import io.agrest.cayenne.cayenne.main.E4; | ||
import io.agrest.cayenne.unit.AgCayenneTester; | ||
import io.agrest.cayenne.unit.DbTest; | ||
import io.agrest.meta.AgEntity; | ||
import io.bootique.junit5.BQTestTool; | ||
import org.apache.cayenne.Cayenne; | ||
import org.apache.cayenne.DataObject; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import javax.ws.rs.PUT; | ||
import javax.ws.rs.Path; | ||
import javax.ws.rs.PathParam; | ||
import javax.ws.rs.core.Configuration; | ||
import javax.ws.rs.core.Context; | ||
import javax.ws.rs.core.UriInfo; | ||
import java.util.List; | ||
|
||
public class PUT_ReadFilterIT extends DbTest { | ||
|
||
@BQTestTool | ||
static final AgCayenneTester tester = tester(Resource.class) | ||
.entities(E2.class, E3.class, E4.class) | ||
.agCustomizer(ab -> ab | ||
.entityOverlay(AgEntity.overlay(E2.class).readFilter(evenFilter())) | ||
.entityOverlay(AgEntity.overlay(E3.class).readFilter(oddFilter())) | ||
.entityOverlay(AgEntity.overlay(E4.class).readFilter(evenFilter())) | ||
) | ||
.build(); | ||
|
||
static <T extends DataObject> ReadFilter<T> evenFilter() { | ||
return o -> Cayenne.intPKForObject(o) % 2 == 0; | ||
} | ||
|
||
static <T extends DataObject> ReadFilter<T> oddFilter() { | ||
return o -> Cayenne.intPKForObject(o) % 2 != 0; | ||
} | ||
|
||
@Test | ||
public void testFilter_InStack() { | ||
|
||
tester.e2().insertColumns("id_", "name") | ||
.values(1, "a") | ||
.values(2, "b") | ||
.values(4, "c") | ||
.exec(); | ||
|
||
// all 3 updates should have been processed, but only the ones matching the read filter should be returned | ||
|
||
tester.target("/e2_stack_filter") | ||
.queryParam("include", "id", "name") | ||
.put("[{\"id\":2,\"name\":\"Bb\"},{\"id\":1,\"name\":\"Aa\"},{\"id\":4,\"name\":\"Cc\"}]") | ||
.wasOk().bodyEquals(2, "{\"id\":2,\"name\":\"Bb\"}", "{\"id\":4,\"name\":\"Cc\"}"); | ||
|
||
tester.e2().matcher().assertMatches(3); | ||
tester.e2().matcher().eq("id_", 1).eq("name", "Aa").assertOneMatch(); | ||
tester.e2().matcher().eq("id_", 2).eq("name", "Bb").assertOneMatch(); | ||
tester.e2().matcher().eq("id_", 4).eq("name", "Cc").assertOneMatch(); | ||
} | ||
|
||
@Test | ||
public void testFilter_InStack_Nested() { | ||
|
||
tester.e2().insertColumns("id_", "name") | ||
.values(1, "a") | ||
.values(2, "b") | ||
.exec(); | ||
|
||
tester.e3().insertColumns("id_", "e2_id") | ||
.values(11, 1) | ||
.values(21, 2) | ||
.values(22, 2) | ||
.exec(); | ||
|
||
// related entity rules must be applied just the same as for the root entity | ||
tester.target("/e2_stack_filter") | ||
.queryParam("include", "id", "e3s.id") | ||
.queryParam("sort", "id") | ||
.put("[{\"id\":2,\"name\":\"Bb\"},{\"id\":1,\"name\":\"Aa\"}]") | ||
.wasOk().bodyEquals(1, "{\"id\":2,\"e3s\":[{\"id\":21}]}"); | ||
} | ||
|
||
@Test | ||
public void testFilter_InStackAndRequest() { | ||
|
||
|
||
tester.e2().insertColumns("id_", "name") | ||
.values(1, "a") | ||
.values(2, "b") | ||
.values(4, "c") | ||
.exec(); | ||
|
||
// all 3 updates should have been processed, but only the one matching the read filter should be returned | ||
|
||
tester.target("/e2_request_and_stack_filter/Bb") | ||
.queryParam("include", "id", "name") | ||
.put("[{\"id\":2,\"name\":\"Bb\"},{\"id\":1,\"name\":\"Aa\"},{\"id\":4,\"name\":\"Cc\"}]") | ||
.wasOk().bodyEquals(1, "{\"id\":2,\"name\":\"Bb\"}"); | ||
|
||
tester.e2().matcher().assertMatches(3); | ||
tester.e2().matcher().eq("id_", 1).eq("name", "Aa").assertOneMatch(); | ||
tester.e2().matcher().eq("id_", 2).eq("name", "Bb").assertOneMatch(); | ||
tester.e2().matcher().eq("id_", 4).eq("name", "Cc").assertOneMatch(); | ||
} | ||
|
||
@Path("") | ||
public static class Resource { | ||
|
||
@Context | ||
private Configuration config; | ||
|
||
@PUT | ||
@Path("e2_stack_filter") | ||
public DataResponse<E2> putE2StackFilter(@Context UriInfo uriInfo, List<EntityUpdate<E2>> updates) { | ||
return Ag.service(config).createOrUpdate(E2.class).uri(uriInfo).syncAndSelect(updates); | ||
} | ||
|
||
@PUT | ||
@Path("e2_request_and_stack_filter/{name}") | ||
public DataResponse<E2> putE2RequestAndStackFilter( | ||
@Context UriInfo uriInfo, | ||
@PathParam("name") String name, | ||
List<EntityUpdate<E2>> updates) { | ||
|
||
return Ag.service(config).createOrUpdate(E2.class) | ||
.uri(uriInfo) | ||
.readableFilter(E2.class, e2 -> name.equals(e2.getName())) | ||
.syncAndSelect(updates); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
agrest-engine/src/main/java/io/agrest/runtime/entity/IResultFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package io.agrest.runtime.entity; | ||
|
||
import io.agrest.RootResourceEntity; | ||
|
||
/** | ||
* @since 4.8 | ||
*/ | ||
public interface IResultFilter { | ||
|
||
<T> void filterTree(RootResourceEntity<T> entity); | ||
} |
104 changes: 104 additions & 0 deletions
104
agrest-engine/src/main/java/io/agrest/runtime/entity/ResultFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package io.agrest.runtime.entity; | ||
|
||
import io.agrest.AgObjectId; | ||
import io.agrest.NestedResourceEntity; | ||
import io.agrest.ResourceEntity; | ||
import io.agrest.RootResourceEntity; | ||
import io.agrest.ToManyResourceEntity; | ||
import io.agrest.ToOneResourceEntity; | ||
import io.agrest.access.ReadFilter; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
/** | ||
* @since 4.8 | ||
*/ | ||
public class ResultFilter implements IResultFilter { | ||
|
||
@Override | ||
public <T> void filterTree(RootResourceEntity<T> entity) { | ||
ReadFilter<T> filter = entity.getAgEntity().getReadFilter(); | ||
if (!filter.allowsAll() && !entity.getResult().isEmpty()) { | ||
|
||
// replacing the list to avoid messing up possible data source caches, and also | ||
// it is likely faster to create a new list than to remove entries from an existing ArrayList | ||
entity.setResult(filterList(entity.getResult(), filter)); | ||
} | ||
|
||
filterChildren(entity); | ||
} | ||
|
||
protected void filterChildren(ResourceEntity<?> entity) { | ||
for (NestedResourceEntity<?> child : entity.getChildren().values()) { | ||
if (child instanceof ToOneResourceEntity) { | ||
filterToOne((ToOneResourceEntity<?>) child); | ||
} else { | ||
filterToMany((ToManyResourceEntity<?>) child); | ||
} | ||
} | ||
} | ||
|
||
protected <T> void filterToOne(ToOneResourceEntity<T> entity) { | ||
|
||
ReadFilter<T> filter = entity.getAgEntity().getReadFilter(); | ||
if (!filter.allowsAll() && !entity.getResultsByParent().isEmpty()) { | ||
|
||
// filter the map in place - key removal should be fast | ||
entity.getResultsByParent().entrySet().removeIf(e -> !filter.isAllowed(e.getValue())); | ||
} | ||
|
||
filterChildren(entity); | ||
} | ||
|
||
protected <T> void filterToMany(ToManyResourceEntity<T> entity) { | ||
|
||
ReadFilter<T> filter = entity.getAgEntity().getReadFilter(); | ||
if (!filter.allowsAll() && !entity.getResultsByParent().isEmpty()) { | ||
|
||
// Filter the map in place; | ||
// Replace relationship lists to avoid messing up possible data source caches, and also | ||
// it is likely faster to create a new list than to remove entries from an existing ArrayList | ||
for (Map.Entry<AgObjectId, List<T>> e : entity.getResultsByParent().entrySet()) { | ||
e.setValue(filterList(e.getValue(), filter)); | ||
} | ||
} | ||
|
||
filterChildren(entity); | ||
} | ||
|
||
static <T> List<T> filterList(List<T> unfiltered, ReadFilter<T> filter) { | ||
|
||
int len = unfiltered.size(); | ||
for (int i = 0; i < len; i++) { | ||
T t = unfiltered.get(i); | ||
if (!filter.isAllowed(t)) { | ||
|
||
// avoid list copy until we can't | ||
return filterListByCopy(unfiltered, filter, i); | ||
} | ||
} | ||
|
||
return unfiltered; | ||
} | ||
|
||
static <T> List<T> filterListByCopy(List<T> unfiltered, ReadFilter<T> filter, int firstExcluded) { | ||
|
||
int len = unfiltered.size(); | ||
List<T> filtered = new ArrayList<>(len - 1); | ||
|
||
for (int i = 0; i < firstExcluded; i++) { | ||
filtered.add(unfiltered.get(i)); | ||
} | ||
|
||
for (int i = firstExcluded + 1; i < len; i++) { | ||
T t = unfiltered.get(i); | ||
if (filter.isAllowed(t)) { | ||
filtered.add(t); | ||
} | ||
} | ||
|
||
return filtered; | ||
} | ||
} |
Oops, something went wrong.