Paging and sorting over Collection - sorting

I want to apply paging and sorting to ArrayList of photos. The list is retrived by rest client. Here is my attempt to paging but returns all 5k elements instead. I try to achive a paging like in JpaRepository. The sorting is done by compareTo() method, and doesn't seem to work properly either.
PhotoServiceImpl.java
private List<Photo> repository;
public PhotoServiceImpl() {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<List<Photo>> photoResponse = restTemplate.exchange("https://jsonplaceholder.typicode.com/photos", HttpMethod.GET, null, new ParameterizedTypeReference<List<Photo>>() {
});
this.repository = photoResponse.getBody();
}
#Override
public List<Photo> findAll() {
return repository;
}
#Override
public Page<Photo> findAll(Pageable pageable) {
List<Photo> photos = findAll();
return new PageImpl<Photo>(photos, new PageRequest(pageable.getPageNumber(), pageable.getPageSize()), photos.size());
}
#Override
public Page<Photo> findAll(Pageable pageable, Sort.Direction sortOrder) {
List<Photo> photos = findAll();
return new PageImpl<Photo>(photos, new
PageRequest(pageable.getPageNumber(), pageable.getPageSize(), sortOrder), photos.size());
}
#Override
public List<Photo> findAll(Sort.Direction sortOrder) {
List<Photo> photos = repository
.stream()
.sorted()
.collect(Collectors.toList());
if (sortOrder.isDescending())
Collections.reverse(photos);
return photos;
}
Photo.java implements Comparable
private int id;
private int albumId;
private String title;
private URL url;
private URL thumbnailUrl;
#Override
public int compareTo(Object o) {
Photo out = ((Photo) o);
int c;
c = Integer.compare(this.getId(), out.getId());
if (c == 0)
c = Integer.compare(this.getAlbumId(), out.getAlbumId());
if (c == 0)
c = this.getTitle().compareTo((out.getTitle()));
return c;
}
}

Getting all photos and the wrapping that in a PageRequest instance with the page size and sorting set will not do what you want. The PageRequest class (or PageImpl class) does not perform the slicing of a list of data into a page or perform the sorting. You must do that yourself
List<Photo> photos = findAll();
//
// << Add your page extraction and sorting code here >>
//
return new PageImpl<Photo>(photos,
new PageRequest(pageable.getPageNumber(), pageable.getPageSize(), sortOrder), photos.size());

Related

Generic Search and Filter by dynamic fields for Criteria (Global Search)

I have a scenario where I need to add Criteria to perform search and filter in Spring using mongoTemplate.
Scenario:
Lets say I have Student, Course and PotentialStudent. and I have to define only certain fields to be used for search and filter purpose. For PotentialStudent, it contains both Student and Course information that is collected before all required information is gathered to be filled to Student and Course.
Search Fields are the fields to be used for searching either of the fields. For example: get values matching in either courseName or courseType in Course.
Filter is to be used to filter specific fields for matching multiple values and the values to be filtered on field is set on FilterParams. Meaning, if I get values in FilterParams.studentType then for PotentialStudent I should
add Criteria to search inside PotentialStudent's student.type for list of values whereas if for Student add Criteria to search in Student's type.
public abstract class Model {
#Id
protected String id;
#CreatedDate
protected Date createdDateTime;
#LastModifiedDate
protected Date modifiedDateTime;
protected abstract List<String> searchFields();
protected abstract Map<String, String> filterFields();
}
#Getter
#Setter
#Document("student")
public class Student extends Model {
private String firstName;
private String lastName;
private String address;
private StudentType type;
#Override
protected List<String> searchFields() {
return Lists.newArrayList("firstName","lastName","address");
}
#Override
protected Map<String, String> filterFields() {
Map<String, String> filterMap = Maps.newHashMap();
filterMap.put("studentType", "type");
return filterMap;
}
}
#Getter
#Setter
#Document("course")
public class Course extends Model {
private String courseName;
private String courseType;
private int duration;
private Difficulty difficulty;
#Override
protected List<String> searchFields() {
return Lists.newArrayList("courseName","courseType");
}
#Override
protected Map<String, String> filterFields() {
Map<String, String> filterMap = Maps.newHashMap();
filterMap.put("courseDifficulty", "difficulty");
return filterMap;
}
}
#Getter
#Setter
#Document("course")
public class PotentialStudent extends Model {
private Student student;
private Course course;
#Override
protected List<String> searchFields() {
return Lists.newArrayList("student.firstName","student.lastName","course.courseName");
}
#Override
protected Map<String, String> filterFields() {
Map<String, String> filterMap = Maps.newHashMap();
filterMap.put("studentType", "student.type");
filterMap.put("courseDifficulty", "course.difficulty");
return filterMap;
}
}
}
public class FilterParams {
private List<StudentType> studentTypes;
private List<Difficulty> difficulties;
}
public class PageData<T extends Model> {
public void setPageRecords(List<T> pageRecords) {
this.pageRecords = pageRecords;
}
private List<T> pageRecords;
}
//Generic Search Filter Implementation Class
public class GenericSearchFilter {
public <T extends Model> PageData getRecordsWithPageSearchFilter(Integer page, Integer size, String sortName, String sortOrder, String value, FilterParams filterParams, Class<T> ormClass) {
PageRequestBuilder pageRequestBuilder = new PageRequestBuilder();
Pageable pageable = pageRequestBuilder.getPageRequest(page, size, sortName, sortOrder);
Query mongoQuery = new Query().with(pageable);
//add Criteria for the domain specified search fields
Criteria searchCriteria = searchCriteria(value, ormClass);
if (searchCriteria != null) {
mongoQuery.addCriteria(searchCriteria);
}
//Handle Filter
query.addCriteria(Criteria.where(filterFields().get("studentType")).in(filterParams.getStudentTypes()));
query.addCriteria(Criteria.where(filterFields().get("courseDifficulty")).in(filterParams.getDifficulty()));
List<T> records = mongoTemplate.find(mongoQuery, ormClass);
PageData pageData = new PageData();
pageData.setPageRecords(records);
return pageData;
}
private <T extends BaseDocument> Criteria searchCriteria(String value, Class<T> ormClass) {
try {
Criteria orCriteria = new Criteria();
if (StringUtils.isNotBlank(value)) {
BaseDocument document = ormClass.getDeclaredConstructor().newInstance();
Method method = ormClass.getDeclaredMethod("searchFields");
List<String> records = (List<String>) method.invoke(document, null);
Criteria[] orCriteriaArray = records.stream().map(s -> Criteria.where(s).regex(value, "i")).toArray(Criteria[]::new);
orCriteria.orOperator(orCriteriaArray);
}
return orCriteria;
} catch (Exception e) {
log.error(e.getMessage());
}
return null;
}
}
Given this scenario, my question is how to handle filter cases in better and dynamic way and how to implement a Global search if needed to search in all Document types for specified fields on each types.

Fetching image from MySQL database in Spring boot and thymeleaf

I am trying to create a service which fetch image of a student along with other text details of student stored in database and display it on an html page. I tried but some hash value is being returned.
My Model Class
#Entity
#Table(name = "details")
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(name = "name")
private String name;
#Column(name = "pic" , length = 2000)
private byte[] pic;
public Student(long id, String name, byte[] pic) {
super();
this.id = id;
this.name = name;
this.pic = pic;
}
public Student() {
super();
// TODO Auto-generated constructor stub
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public byte[] getPic() {
return pic;
}
public void setPic(byte[] pic) {
this.pic = pic;
}
#Override
public String toString() {
return "Student [id=" + id + ", name=" + name + "];
}
}
Without Using a rest contoller will it be achieved like this?
My Controller
#Controller
public class ImgShowController {
#Autowired
EntityRepository entityRepository;
#RequestMapping("/list")
public String getAllStudents(Model model) {
List<Imge> list = (List<Imge>) entityRepository.findAll();
model.addAttribute("students", list);
return "liststudents";
}
#RequestMapping(path= {"/particularlist","/particularlist/{id}"})
public String getImage(#PathVariable("id") Long id, Model model) {
final Optional<Student> imget = entityRepository.findById(id);
Imge imge = new Imge(imget.get().getId(), imget.get().getName(), decompressBytes(imget.get().getPic()));
model.addAttribute("particularStudent", imge);
return "particularstudent";
}
}
Decompress Byte function
public static byte[] decompressBytes(byte[] data) {
Inflater inflater = new Inflater();
inflater.setInput(data);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length);
byte[] buffer = new byte[1024];
try {
while (!inflater.finished()) {
int count = inflater.inflate(buffer);
outputStream.write(buffer, 0, count);
}
outputStream.close();
} catch (IOException ioe) {
} catch (DataFormatException e) {
}
return outputStream.toByteArray();
}
First of all, I would suggest mentioning that you store binary object (LOB) in pic column in your entity class:
#Lob
#Column(name = "pic" , length = 2000)
private byte[] pic;
And then, it seems that Thymeleaf does not allow you to inject image directly into model, so you have 2 ways to accomplish this:
1.Adding another controller to serve you images
#GetMapping("/students/{id}/image")
public void studentImage(#PathVariable String id, HttpServletResponse response) throws IOException {
var student = entityRepository.findById(id);
var imageDecompressed = decompressBytes(student.get().getPic());
response.setContentType("image/jpeg");
InputStream is = new ByteArrayInputStream(imageDecompressed);
IOUtils(is, response.getOutputStream());
}
and then referring to it from model like this:
<img th:src="#{'students/' + #{studentId} + '/image'}">
2.Using base64
You need to encode image as base64 string:
var base64EncodedImage = Base64.getEncoder().encodeToString(imageData);
and then setting into model like this:
<img th:src="#{'data:image/jpeg;base64,'+${base64EncodedImage}}"/>
I would suggest using the first way because otherwise you would depend on image size and overall payload would be 30% larger (base64), so by using the first way you let user's browser decide how and when to load particular image

How can I add data to CRUD in Vaadin by using setItems for grid?

I'm going to add data to a CRUD component in Vaadin. It's an easy question here.
But the issue I got is that I cannot add data to the CRUD by first getting the grid object and then set its items to it.
Here is my Vaadin class. This class begins first to get data from a JPA Spring database. OK. That's works. And the data is transfered into a collection named crudData. Then the crudData is beings set to crud.getGrid().setItems(crudData); and that's not working. I assume that if I get the grid from the CRUD, then I can set the grid items as well too and then they will show up on the CRUD....but no...
#Data
public class StocksCrud {
private Crud<StockNames> crud;
private List<StockNames> crudData;
private StockNamesRepository stockNamesRepository;
private CrudEditor<StockNames> createStocksEditor() {
TextField stockName = new TextField("Name of the stock");
FormLayout form = new FormLayout(stockName);
Binder<StockNames> binder = new Binder<>(StockNames.class);
binder.bind(stockName, StockNames::getStockName, StockNames::setStockName);
return new BinderCrudEditor<>(binder, form);
}
public StocksCrud(StockNamesRepository stockNamesRepository) {
this.stockNamesRepository = stockNamesRepository;
// Fill the crud
crudData = new ArrayList<StockNames>();
for(StockNames stockName: stockNamesRepository.findAll()) {
crudData.add(new StockNames(stockName.getId(), stockName.getStockName()));
}
// Crate crud table
crud = new Crud<>(StockNames.class, createStocksEditor());
crud.getGrid().setItems(crudData); // This won't work
crud.addSaveListener(e -> saveStock(e.getItem()));
crud.addDeleteListener(e -> deleteStock(e.getItem()));
crud.getGrid().removeColumnByKey("id");
crud.addThemeVariants(CrudVariant.NO_BORDER);
}
private void deleteStock(StockNames stockNames) {
boolean exist = stockNamesRepository.existsBystockName(stockNames.getStockName());
if(exist == true) {
crudData.remove(stockNames);
stockNamesRepository.delete(stockNames);
}
}
private void saveStock(StockNames stockNames) {
System.out.println(stockNames == null);
System.out.println(stockNamesRepository == null);
boolean exist = stockNamesRepository.existsBystockName(stockNames.getStockName());
if(exist == false) {
crudData.add(stockNames);
stockNamesRepository.save(stockNames);
}
}
}
Here is my error output:
java.lang.ClassCastException: com.vaadin.flow.component.crud.CrudFilter cannot be cast to com.vaadin.flow.function.SerializablePredicate
I know that there is a way to set data to CRUD in Vaadin, by using a data provider class. But I don't want to use that. It's....to much code. I want to keep it clean and write less code in Java. Example here at the bottom: https://vaadin.com/components/vaadin-crud/java-examples
#Entity
#Data
#NoArgsConstructor
public class StockNames implements Cloneable{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String stockName;
public StockNames(int id, String stockName) {
this.id = id;
this.stockName = stockName;
}
}
Update:
This is my code now
#Data
public class StocksCrud {
private Crud<StockNames> crud;
private List<StockNames> crudData;
private StockNamesRepository stockNamesRepository;
private CrudEditor<StockNames> createStocksEditor() {
TextField stockName = new TextField("Name of the stock");
FormLayout form = new FormLayout(stockName);
Binder<StockNames> binder = new Binder<>(StockNames.class);
binder.bind(stockName, StockNames::getStockName, StockNames::setStockName);
return new BinderCrudEditor<>(binder, form);
}
public StocksCrud(StockNamesRepository stockNamesRepository) {
this.stockNamesRepository = stockNamesRepository;
// Fill the crud
crudData = new ArrayList<StockNames>();
for(StockNames stockName: stockNamesRepository.findAll()) {
crudData.add(new StockNames(stockName.getId(), stockName.getStockName()));
}
// Create grid
Grid<StockNames> grid = new Grid<StockNames>();
grid.setItems(crudData);
// Crate crud table
crud = new Crud<>(StockNames.class, createStocksEditor());
crud.setGrid(grid);
crud.addSaveListener(e -> saveStock(e.getItem()));
crud.addDeleteListener(e -> deleteStock(e.getItem()));
//crud.getGrid().removeColumnByKey("id");
crud.addThemeVariants(CrudVariant.NO_BORDER);
}
private void deleteStock(StockNames stockNames) {
boolean exist = stockNamesRepository.existsBystockName(stockNames.getStockName());
if(exist == true) {
crudData.remove(stockNames);
stockNamesRepository.delete(stockNames);
}
}
private void saveStock(StockNames stockNames) {
System.out.println(stockNames == null);
System.out.println(stockNamesRepository == null);
boolean exist = stockNamesRepository.existsBystockName(stockNames.getStockName());
if(exist == false) {
crudData.add(stockNames);
stockNamesRepository.save(stockNames);
}
}
}
Update 2:
This gives an error.
// Create grid
Grid<StockNames> grid = new Grid<StockNames>();
StockNames s1 = new StockNames(1, "HELLO");
crudData.add(s1);
grid.setItems(crudData);
// Crate crud table
crud = new Crud<>(StockNames.class, createStocksEditor());
crud.setGrid(grid);
crud.addSaveListener(e -> saveStock(e.getItem()));
crud.addDeleteListener(e -> deleteStock(e.getItem()));
crud.getGrid().removeColumnByKey(grid.getColumns().get(0).getKey());
crud.addThemeVariants(CrudVariant.NO_BORDER);
The error is:
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0'
What? I just added a object.
Assigning grid fixes the issue
Grid<StockNames> grid=new Grid<>(StockNames.class);
crud = new Crud<>(StockNames.class,grid, createStocksEditor());
In your code example you are relying on default implementation provided by Crud, thus CrudGrid is getting created. Its setDataProvider returns DataProvider<E,CrudFilter>, whereas Grid's DataProvider is of type: AbstractDataProvider<T, SerializablePredicate<T>> (This is because you are using ListDataProvider, which extends AbstractDataProvider<T, SerializablePredicate<T>>). This is what error states:
java.lang.ClassCastException: com.vaadin.flow.component.crud.CrudFilter cannot be cast to com.vaadin.flow.function.SerializablePredicate
So if you want to assign values via grid- you would first need to create one. Otherwise, as shown in the docs you could provide a custom dataprovider: PersonDataProvider
Update
This is an example code I am using. Adding a new item in Crud works, after I have added a no-args constructor to the bean:
import java.util.Random;
public class StockNames implements Cloneable{
Random rnd=new Random();
private int id;
private String stockName;
public StockNames(){
//You will an id generated automatically for you, but here is just an example
id=rnd.nextInt(12000);
}
public StockNames(int id, String stockName) {
this.id = id;
this.stockName = stockName;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getStockName() {
return stockName;
}
public void setStockName(String stockName) {
this.stockName = stockName;
}
}
and the StockCrud class:
import com.vaadin.flow.component.crud.*;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.router.Route;
import java.util.ArrayList;
import java.util.List;
#Route("crudLayout")
public class StockCrud extends VerticalLayout {
private Crud<StockNames> crud;
private List<StockNames> crudData;
private CrudEditor<StockNames> createStocksEditor() {
TextField stockName = new TextField("Name of the stock");
FormLayout form = new FormLayout(stockName);
Binder<StockNames> binder = new Binder<>(StockNames.class);
binder.bind(stockName, StockNames::getStockName, StockNames::setStockName);
return new BinderCrudEditor<>(binder, form);
}
public StockCrud() {
// Fill the crud
crudData = new ArrayList<StockNames>();
for(int i=0;i<150;i++) {
crudData.add(new StockNames(i,"Name " + i));
}
// Crate crud table
Grid<StockNames> grid=new Grid<>(StockNames.class);
crud = new Crud<>(StockNames.class,grid, createStocksEditor());
//((CrudGrid )crud.getGrid()).setItems(crudData);
crud.getGrid().setItems(crudData); // This won't work
crud.addSaveListener(e -> saveStock(e.getItem()));
crud.addDeleteListener(e -> deleteStock(e.getItem()));
// crud.getGrid().removeColumnByKey("id");
crud.addThemeVariants(CrudVariant.NO_BORDER);
add(crud);
}
private void deleteStock(StockNames stockNames) {
// if(crudData.contains(stockNames)) {
crudData.remove(stockNames);
//}
}
private void saveStock(StockNames stockNames) {
System.out.println(stockNames == null);
if(!crudData.contains(stockNames)) {
crudData.add(stockNames);
}
}
}

Cannot remove attributes in ldap with spring ldap

we need to make a spring boot project that works with spring ldap.
every things is good.But when we remove a member from a group,the member deleted form group (i see it in debug mode in a Setmembers) but, in ldap(Oracle Internet Directory) that member exists!
Please help me!
//Group Entry
#Entry(objectClasses = {"top", "groupOfUniqueNames", "orclGroup"}, base = "cn=Groups")
public final class Group {
#Id
private Name dn;
#Attribute(name = "cn")
private String name;
private String description;
private String displayName;
#Attribute(name = "ou")
private String ou;
#Attribute(name = "uniqueMember")
private Set<Name> members;
public void addMember(Name newMember) {
members.add(newMember);
}
public void removeMember(Name member) {
members.remove(member);
}
//Custom LdapUtils
public class CustomLdapUtils {
private static final String GROUP_BASE_DN = "cn=Groups";
private static final String USER_BASE_DN = "cn=Users";
public Name buildGroupDn(String name) {
return LdapNameBuilder.newInstance(GROUP_BASE_DN)
.add("cn","Charts")
.add("cn",name)
.build();
}
private static final CsutomLdapUtils LDAP_UTILS = new CsutomLdapUtils ();
private CsutomLdapUtils () {
}
public Name buildPersonDn(String name) {
return LdapNameBuilder.newInstance(USER_BASE_DN)
.add("cn", name)
.build();
}
}
//Controller
#DeleteMapping(value = "/memberOfGroup", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> removeMemberFromGroup(#RequestBody Map<String,String> map) throws NamingException {
List<Group> groupToFind = ldapSearchGroupsService.getGroupByCn(map.get("groupName"));
List<User> userToFind = ldapSearchUserService.getAllUserByUserName(map.get("userName"));
if (groupToFind.isEmpty()) {
//TODO : Group no found!
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} else {
for (Group group1 : groupToFind) {
group1.removeMember(userToFind.stream().findAny().get().getDn());
//ldapBindGroupService.deleteMemberFromGroup(group1);
DirContextOperations ctx = ldapTemplate.lookupContext(CustomLdapUtils.getInstance().buildGroupDn(map.get("groupName")));
ctx.removeAttributeValue("uniqueMember",map.get("userName"));
ctx.rebind(CustomLdapUtils.getInstance().buildGroupDn(map.get("groupName")),map.get("groupName"));
ldapTemplate.modifyAttributes(ctx);
}
return new ResponseEntity<>(HttpStatus.OK);
}
}
Is some problem in code? or need some methods?
Finally after several search and debug,i found the problem!
In each ldap env,after every changes,the directory must be commit and apply.
In above code,i implemented that,but not in true way!
Best way is here:
#DeleteMapping(value = "/membersOfGroup", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> removeMemberFromGroup(#RequestBody Map<String,String> map) {
List<Group> groupToFind = ldapSearchGroupsService.getGroupByCn(map.get("groupName"));
List<User> userToFind = ldapSearchUserService.getAllUserByUserName(map.get("userName"));
if (groupToFind.isEmpty()) {
//TODO : Group no found!
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} else {
for (Group group1 : groupToFind) {
group1.removeMember(userToFind.stream().findAny().get().getDn());
DirContextOperations ctx = ldapTemplate.lookupContext(CustomLdapUtils.getInstance().buildGroupDn(map.get("groupName")));
ctx.removeAttributeValue("member",CustomLdapUtils.getInstance().buildPersonDn(map.get("userName")));
//True way
ldapTemplate.update(group1);
}
return new ResponseEntity<>(HttpStatus.OK);
}
}

Dynamic MongoDB collection in spring boot

I want to create a MongoDB collection for each month dynamically.
Example: viewLog_01_2018, viewLog_02_2018
#Document(collection = "#{viewLogRepositoryImpl.getCollectionName()}")
#CompoundIndexes({
#CompoundIndex(def = "{'viewer':1, 'viewed':1}", name = "viewerViewedIndex",unique=true)
})
public class ViewLog {
private Integer viewer;
private Integer viewed;
private Date time;
public Integer getViewer() {
return viewer;
}
public void setViewer(Integer viewer) {
this.viewer = viewer;
}
public Integer getViewed() {
return viewed;
}
public void setViewed(Integer viewed) {
this.viewed = viewed;
}
public Date getTime() {
return time;
}
public void setTime(Date time) {
this.time = time;
}
}
The implementation for the collection name is as follows:
#Repository
public class ViewLogRepositoryImpl implements ViewLogRepositoryCustom {
private String collectionName;
public ViewLogRepositoryImpl() {
CommonUtility common = new CommonUtility();
Pair<Integer, Integer> pair = common.getStartingEndingDateOfMonth();
setCollectionName("viewLog_"+pair.getFirst()+"_"+pair.getSecond());
}
#Override
public String getCollectionName() {
return collectionName;
}
#Override
public void setCollectionName(String collectionName) {
this.collectionName = collectionName;
}
}
On my each request, to save a document, I am setting the collection name as:
#Autowired
ViewLogRepository viewLogRepository;
public boolean createLog(int viewer, int viewed,String viewed_mmm, Date time){
CommonUtility common = new CommonUtility();
Pair<Integer, Integer> pair = common.getStartingEndingDateOfMonth();
viewLogRepository.setCollectionName("viewLog_"+pair.getFirst()+"_"+pair.getSecond());
ViewLog viewLog = new ViewLog();
viewLog.setViewer(viewer);
viewLog.setViewed(viewed);
viewLog.setTime(time);
ViewLog viewLog2 = viewLogRepository.save(viewLog);
return true;
}
The problem I am facing is that I when for the first time I up my service the mongo collection that is created has the unique attribute for the fields 'viewer' and 'viewed' but for any subsequent collection that is created dynamically, the document does not have the unique constraint and multiple entries of same viewer-viewed combination are able to be inserted.
Any help will be very much appreciated.

Resources