Compare commits
54 Commits
wip-new-ev
...
wip-custom
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ae6db7229 | ||
|
|
c03a5fed8d | ||
|
|
ee45163f2c | ||
|
|
e6bf638b4b | ||
|
|
d6f60101db | ||
|
|
e54577d656 | ||
|
|
bae00f6bd7 | ||
|
|
68dca23a6b | ||
|
|
19c9f88a7f | ||
|
|
9b6956b8df | ||
|
|
e2de325df2 | ||
|
|
c9fa916cdd | ||
|
|
f79ebb4d18 | ||
|
|
8f2fc83a34 | ||
|
|
05f2f309e7 | ||
|
|
6cf774da2a | ||
|
|
3c2e9d374c | ||
|
|
55cb34ef4f | ||
|
|
dde554e442 | ||
|
|
fd75779093 | ||
|
|
c8291bec71 | ||
|
|
1005c47d83 | ||
|
|
47e9053285 | ||
|
|
f76912a6cf | ||
|
|
625ea6007e | ||
|
|
6fae59fdeb | ||
|
|
f846a32d95 | ||
|
|
2a712017f1 | ||
|
|
85613936f4 | ||
|
|
6f480ad11a | ||
|
|
2b0c405378 | ||
|
|
a5d1e7312c | ||
|
|
29d42fda9a | ||
|
|
1b53bd9147 | ||
|
|
d1328e4ce8 | ||
|
|
45bda8e14d | ||
|
|
d9e13ff669 | ||
|
|
1d14ece9cf | ||
|
|
1e13d482a8 | ||
|
|
2467099c3e | ||
|
|
f4ec33d275 | ||
|
|
a91ade08b1 | ||
|
|
fb97767a06 | ||
|
|
1e7234166a | ||
|
|
d0bdd51406 | ||
|
|
f4e070e7bd | ||
|
|
bd3de1a938 | ||
|
|
f570ccbe90 | ||
|
|
e488df3d06 | ||
|
|
4f11433390 | ||
|
|
b573027fc2 | ||
|
|
3117f12402 | ||
|
|
5b029d8307 | ||
|
|
6d8376cfd5 |
18
README.md
18
README.md
@@ -1,7 +1,7 @@
|
||||
#Event-Sourcing+CQRS example application
|
||||
|
||||
This example application is the money transfer application described in my talk [Building and deploying microservices with event sourcing, CQRS and Docker](http://plainoldobjects.com/presentations/building-and-deploying-microservices-with-event-sourcing-cqrs-and-docker/).
|
||||
This talk describe a way of architecting highly scalable and available applications that is based on microservices, polyglot persistence,
|
||||
This talk describes a way of architecting highly scalable and available applications that is based on microservices, polyglot persistence,
|
||||
event sourcing (ES) and command query responsibility segregation (CQRS).
|
||||
Applications consist of loosely coupled components that communicate using events.
|
||||
These components can be deployed either as separate services or packaged as a monolithic application for simplified development and testing.
|
||||
@@ -94,9 +94,12 @@ First, you need to tell the query side code how to connect to MongoDB:
|
||||
```
|
||||
|
||||
[Docker Compose](https://docs.docker.com/compose/) is a great way to run MongoDB.
|
||||
You can run the `docker-compose up -d mongodb` to run MongoDB.
|
||||
You can run the `docker-compose up -d mongodb` to run MongoDB and then set `SPRING_DATA_MONGODB_URI` as follows:
|
||||
```
|
||||
export SPRING_DATA_MONGODB_URI=mongodb://$(docker-machine ip default)/yourdb
|
||||
```
|
||||
|
||||
Second, some of the tests in accounts-command-side-service, transactions-command-side-service, accounts-query-side-service and e2e-test need you need to set some environment variables that tell them how to connect to the Event Store server.
|
||||
Second, some of the tests in accounts-command-side-service, transactions-command-side-service, accounts-query-side-service and e2e-test require you to set some environment variables that tell them how to connect to the Event Store server.
|
||||
But don't worry.
|
||||
The build is configured to ignore failures for those projects.
|
||||
|
||||
@@ -117,6 +120,15 @@ Simply use this command:
|
||||
java -jar monolithic-service/build/libs/monolithic-service.jar
|
||||
```
|
||||
|
||||
This will start the service running on port 8080 (you can change using the --server.port=9999 option).
|
||||
|
||||
Once the service has started you can open the Swagger UI: http://localhost:8080/swagger-ui.html.
|
||||
You can then:
|
||||
|
||||
1. Create two accounts (save the account ids)
|
||||
2. Create a money transfer
|
||||
3. View the updated account balances
|
||||
|
||||
## Running the microservices
|
||||
|
||||
The other option is to run the services separately.
|
||||
|
||||
@@ -6,6 +6,12 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
DOCKER_COMPOSE="docker-compose -p event-sourcing-examples"
|
||||
|
||||
if [ "$1" = "-f" ] ; then
|
||||
shift;
|
||||
DOCKER_COMPOSE="$DOCKER_COMPOSE -f ${1?}"
|
||||
shift
|
||||
fi
|
||||
|
||||
if [ "$1" = "--use-existing" ] ; then
|
||||
shift;
|
||||
else
|
||||
@@ -13,6 +19,13 @@ else
|
||||
${DOCKER_COMPOSE?} rm -v --force
|
||||
fi
|
||||
|
||||
NO_RM=false
|
||||
|
||||
if [ "$1" = "--no-rm" ] ; then
|
||||
NO_RM=true
|
||||
shift
|
||||
fi
|
||||
|
||||
${DOCKER_COMPOSE?} up -d mongodb
|
||||
|
||||
if [ -z "$DOCKER_HOST_IP" ] ; then
|
||||
@@ -47,5 +60,7 @@ set -e
|
||||
|
||||
./gradlew -a $* :e2e-test:cleanTest :e2e-test:test -P ignoreE2EFailures=false
|
||||
|
||||
${DOCKER_COMPOSE?} stop
|
||||
${DOCKER_COMPOSE?} rm -v --force
|
||||
if [ $NO_RM = false ] ; then
|
||||
${DOCKER_COMPOSE?} stop
|
||||
${DOCKER_COMPOSE?} rm -v --force
|
||||
fi
|
||||
|
||||
@@ -7,19 +7,19 @@ This application consists of three microservices:
|
||||
* Account Service - the command side business logic for Accounts
|
||||
* Money Transfer Service - the command side business logic for Money Transfers
|
||||
* Query service - query side implementation of a MongoDB-based, denormalized view of Accounts and MoneyTransfers
|
||||
|
||||
|
||||
The Account Service consists of the following modules:
|
||||
|
||||
* accounts-command-side-backend - the Account aggregate
|
||||
* accounts-command-side-web - a REST API for creating and retrieving Accounts
|
||||
* accounts-command-side-service - a standalone microservice
|
||||
|
||||
|
||||
The Money Transfer Service consists of the following modules:
|
||||
|
||||
* transactions-command-side-backend - the MoneyTransfer aggregate
|
||||
* transactions-command-side-web - a REST API for creating and retrieving Money Transfers
|
||||
* transactions-command-side-service - a standalone microservice
|
||||
|
||||
|
||||
The Query Service consists the following modules:
|
||||
|
||||
* accounts-query-side-backend - MongoDB-based, denormalized view of Accounts and MoneyTransfers
|
||||
@@ -28,10 +28,8 @@ The Query Service consists the following modules:
|
||||
|
||||
# Deploying the application
|
||||
|
||||
These services can be deployed either as either separate standalone services using the Event Store server, or they can be deployed as a monolithic application for simpified integration testing.
|
||||
These services can be deployed either as either separate standalone services using the Event Store server, or they can be deployed as a monolithic application for simplified integration testing.
|
||||
|
||||
The three services can also be packaged as a single monolithic web application in order to be used with the embedded Event Store:
|
||||
|
||||
* monolithic-service - all-in-one, monolithic packaging of the application
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
apply plugin: VerifyMongoDBConfigurationPlugin
|
||||
apply plugin: VerifyEventStoreEnvironmentPlugin
|
||||
apply plugin: EventuateDependencyPlugin
|
||||
|
||||
apply plugin: 'spring-boot'
|
||||
|
||||
@@ -10,9 +11,6 @@ dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-web"
|
||||
compile "org.springframework.boot:spring-boot-starter-actuator"
|
||||
|
||||
|
||||
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
|
||||
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test"
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public class AccountsCommandSideServiceIntegrationTest {
|
||||
private int port;
|
||||
|
||||
private String baseUrl(String path) {
|
||||
return "http://localhost:" + port + "/" + path;
|
||||
return "http://localhost:" + port + "/api" + path;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/accounts")
|
||||
@RequestMapping("/api/accounts")
|
||||
public class AccountController {
|
||||
|
||||
private AccountService accountService;
|
||||
|
||||
@@ -35,7 +35,7 @@ public class AccountControllerIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void shouldCreateAccount() throws Exception {
|
||||
mockMvc.perform(post("/accounts")
|
||||
mockMvc.perform(post("/api/accounts")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"customerId\" : \"00000000-00000000\", \"initialBalance\" : 500}")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
@@ -44,7 +44,7 @@ public class AccountControllerIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void shouldRejectBadRequest() throws Exception {
|
||||
mockMvc.perform(post("/accounts")
|
||||
mockMvc.perform(post("/api/accounts")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"initialBalanceXXX\" : 500}")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
public class AccountChangeInfo {
|
||||
|
||||
private String changeId;
|
||||
private String transactionId;
|
||||
private String transactionType;
|
||||
private long amount;
|
||||
private long balanceDelta;
|
||||
|
||||
public AccountChangeInfo(String changeId, String transactionId, String transactionType, long amount, long balanceDelta) {
|
||||
this.changeId = changeId;
|
||||
this.transactionId = transactionId;
|
||||
this.transactionType = transactionType;
|
||||
this.amount = amount;
|
||||
this.balanceDelta = balanceDelta;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Created by cer on 11/21/14.
|
||||
@@ -15,13 +17,19 @@ public class AccountInfo {
|
||||
private String description;
|
||||
private long balance;
|
||||
private List<AccountChangeInfo> changes;
|
||||
private List<AccountTransactionInfo> transactions;
|
||||
private Map<String, AccountTransactionInfo> transactions;
|
||||
private Map<String, TransferState> transferStates;
|
||||
private String version;
|
||||
private Date date;
|
||||
|
||||
private AccountInfo() {
|
||||
}
|
||||
|
||||
public AccountInfo(String id, String customerId, String title, String description, long balance, List<AccountChangeInfo> changes, List<AccountTransactionInfo> transactions, String version) {
|
||||
public AccountInfo(String id, String customerId, String title, String description, long balance, List<AccountChangeInfo> changes, Map<String, AccountTransactionInfo> transactions, String version) {
|
||||
this(id, customerId, title, description, balance, changes, transactions, version, new Date());
|
||||
}
|
||||
|
||||
public AccountInfo(String id, String customerId, String title, String description, long balance, List<AccountChangeInfo> changes, Map<String, AccountTransactionInfo> transactions, String version, Date date) {
|
||||
|
||||
this.id = id;
|
||||
this.customerId = customerId;
|
||||
@@ -31,6 +39,7 @@ public class AccountInfo {
|
||||
this.changes = changes;
|
||||
this.transactions = transactions;
|
||||
this.version = version;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
@@ -54,14 +63,26 @@ public class AccountInfo {
|
||||
}
|
||||
|
||||
public List<AccountChangeInfo> getChanges() {
|
||||
return changes;
|
||||
return changes == null ? Collections.EMPTY_LIST : changes;
|
||||
}
|
||||
|
||||
public List<AccountTransactionInfo> getTransactions() {
|
||||
return transactions;
|
||||
return transactions == null ? Collections.EMPTY_LIST : new ArrayList<>(transactions.values());
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public Map<String, TransferState> getTransferStates() {
|
||||
return transferStates;
|
||||
}
|
||||
|
||||
public void setTransferStates(Map<String, TransferState> transferStates) {
|
||||
this.transferStates = transferStates;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
import com.mongodb.WriteResult;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.data.mongodb.core.query.Update;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
||||
import static net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.MoneyUtil.toIntegerRepr;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.where;
|
||||
@@ -28,29 +31,33 @@ public class AccountInfoUpdateService {
|
||||
|
||||
public void create(String accountId, String customerId, String title, BigDecimal initialBalance, String description, String version) {
|
||||
try {
|
||||
accountInfoRepository.save(new AccountInfo(
|
||||
accountId,
|
||||
customerId,
|
||||
title,
|
||||
description,
|
||||
toIntegerRepr(initialBalance),
|
||||
Collections.<AccountChangeInfo>emptyList(),
|
||||
Collections.<AccountTransactionInfo>emptyList(),
|
||||
version));
|
||||
AccountChangeInfo ci = new AccountChangeInfo();
|
||||
ci.setAmount(toIntegerRepr(initialBalance));
|
||||
WriteResult x = mongoTemplate.upsert(new Query(where("id").is(accountId).and("version").exists(false)),
|
||||
new Update()
|
||||
.set("customerId", customerId)
|
||||
.set("title", title)
|
||||
.set("description", description)
|
||||
.set("balance", toIntegerRepr(initialBalance))
|
||||
.push("changes", ci)
|
||||
.set("date", new Date())
|
||||
.set("version", version),
|
||||
AccountInfo.class);
|
||||
logger.info("Saved in mongo");
|
||||
|
||||
} catch (DuplicateKeyException t) {
|
||||
logger.warn("When saving ", t);
|
||||
} catch (Throwable t) {
|
||||
logger.error("Error during saving: ");
|
||||
logger.error("Error during saving: ", t);
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void addTransaction(String eventId, String fromAccountId, AccountTransactionInfo ti) {
|
||||
mongoTemplate.updateMulti(new Query(where("id").is(fromAccountId)), /* wrong .and("version").lt(eventId) */
|
||||
public void addTransaction(String accountId, AccountTransactionInfo ti) {
|
||||
mongoTemplate.upsert(new Query(where("id").is(accountId)),
|
||||
new Update().
|
||||
push("transactions", ti).
|
||||
set("version", eventId),
|
||||
set("transactions." + ti.getTransactionId(), ti),
|
||||
AccountInfo.class);
|
||||
}
|
||||
|
||||
@@ -64,5 +71,10 @@ public class AccountInfoUpdateService {
|
||||
AccountInfo.class);
|
||||
}
|
||||
|
||||
|
||||
public void updateTransactionStatus(String accountId, String transactionId, TransferState status) {
|
||||
mongoTemplate.upsert(new Query(where("id").is(accountId)),
|
||||
new Update().
|
||||
set("transferStates." + transactionId, status),
|
||||
AccountInfo.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
import io.eventuate.CompletableFutureUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class AccountQueryService {
|
||||
|
||||
@@ -13,15 +10,17 @@ public class AccountQueryService {
|
||||
this.accountInfoRepository = accountInfoRepository;
|
||||
}
|
||||
|
||||
public CompletableFuture<AccountInfo> findByAccountId(String accountId) {
|
||||
public AccountInfo findByAccountId(String accountId) {
|
||||
AccountInfo account = accountInfoRepository.findOne(accountId);
|
||||
if (account == null)
|
||||
return CompletableFutureUtil.failedFuture(new AccountNotFoundException(accountId));
|
||||
throw new AccountNotFoundException(accountId);
|
||||
else
|
||||
return CompletableFuture.completedFuture(account);
|
||||
if(account.getTransferStates()!=null)
|
||||
account.getTransactions().stream().forEach(ati -> ati.setStatus(account.getTransferStates().get(ati.getTransactionId())));
|
||||
return account;
|
||||
}
|
||||
|
||||
public CompletableFuture<List<AccountInfo>> findByCustomerId(String customerId) {
|
||||
return CompletableFuture.completedFuture(accountInfoRepository.findByCustomerId(customerId));
|
||||
public List<AccountInfo> findByCustomerId(String customerId) {
|
||||
return accountInfoRepository.findByCustomerId(customerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.ac
|
||||
import io.eventuate.DispatchedEvent;
|
||||
import io.eventuate.EventHandlerMethod;
|
||||
import io.eventuate.EventSubscriber;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountChangedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountCreditedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountDebitedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountOpenedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.*;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.CreditRecordedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.DebitRecordedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.FailedDebitRecordedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.MoneyTransferCreatedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -57,20 +59,36 @@ public class AccountQueryWorkflow {
|
||||
de.getEvent().getDetails().getDate(),
|
||||
de.getEvent().getDetails().getDescription());
|
||||
|
||||
accountInfoUpdateService.addTransaction(eventId, fromAccountId, ti);
|
||||
accountInfoUpdateService.addTransaction(eventId, toAccountId, ti);
|
||||
accountInfoUpdateService.addTransaction(fromAccountId, ti);
|
||||
accountInfoUpdateService.addTransaction(toAccountId, ti);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public void recordDebit(DispatchedEvent<AccountDebitedEvent> de) {
|
||||
String accountId = de.getEntityId();
|
||||
String transactionId = de.getEvent().getTransactionId();
|
||||
|
||||
accountInfoUpdateService.updateTransactionStatus(accountId, transactionId, TransferState.DEBITED);
|
||||
saveChange(de, -1);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public void recordCredit(DispatchedEvent<AccountCreditedEvent> de) {
|
||||
String accountId = de.getEntityId();
|
||||
String transactionId = de.getEvent().getTransactionId();
|
||||
|
||||
accountInfoUpdateService.updateTransactionStatus(accountId, transactionId, TransferState.COMPLETED);
|
||||
saveChange(de, +1);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public void recordFailed(DispatchedEvent<AccountDebitFailedDueToInsufficientFundsEvent> de) {
|
||||
String accountId = de.getEntityId();
|
||||
String transactionId = de.getEvent().getTransactionId();
|
||||
|
||||
accountInfoUpdateService.updateTransactionStatus(accountId, transactionId, TransferState.FAILED_DUE_TO_INSUFFICIENT_FUNDS);
|
||||
}
|
||||
|
||||
public <T extends AccountChangedEvent> void saveChange(DispatchedEvent<T> de, int delta) {
|
||||
String changeId = de.getEventId().asString();
|
||||
String transactionId = de.getEvent().getTransactionId();
|
||||
|
||||
@@ -27,8 +27,6 @@ public class QuerySideAccountConfiguration {
|
||||
return new AccountQueryService(accountInfoRepository);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public QuerySideDependencyChecker querysideDependencyChecker(MongoTemplate mongoTemplate) {
|
||||
return new QuerySideDependencyChecker(mongoTemplate);
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
|
||||
import io.eventuate.javaclient.spring.jdbc.IdGenerator;
|
||||
import io.eventuate.javaclient.spring.jdbc.IdGeneratorImpl;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountCreditedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.IntegrationTest;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = AccountInfoUpdateServiceTest.AccountInfoUpdateServiceTestConfiguration.class)
|
||||
@IntegrationTest
|
||||
public class AccountInfoUpdateServiceTest {
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@Import({QuerySideAccountConfiguration.class, EventuateJdbcEventStoreConfiguration.class})
|
||||
public static class AccountInfoUpdateServiceTestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private AccountInfoUpdateService accountInfoUpdateService;
|
||||
|
||||
@Autowired
|
||||
private AccountQueryService accountQueryService;
|
||||
|
||||
@Test
|
||||
public void shouldSaveAccountInfo() throws ExecutionException, InterruptedException {
|
||||
IdGenerator x = new IdGeneratorImpl();
|
||||
String accountId = x.genId().asString();
|
||||
String customerId = x.genId().asString();
|
||||
String version = x.genId().asString();
|
||||
|
||||
String title = "Checking account";
|
||||
BigDecimal initialBalance = new BigDecimal("1345");
|
||||
String description = "Some account";
|
||||
|
||||
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
|
||||
|
||||
AccountInfo accountInfo = accountQueryService.findByAccountId(accountId);
|
||||
|
||||
assertEquals(accountId, accountInfo.getId());
|
||||
assertEquals(customerId, accountInfo.getCustomerId());
|
||||
assertEquals(title, accountInfo.getTitle());
|
||||
assertEquals(description, accountInfo.getDescription());
|
||||
assertEquals(initialBalance.longValue() * 100, accountInfo.getBalance());
|
||||
assertEquals(1, accountInfo.getChanges().size());
|
||||
assertTrue(accountInfo.getTransactions().isEmpty());
|
||||
assertEquals(version, accountInfo.getVersion());
|
||||
|
||||
|
||||
String changeId = x.genId().asString();
|
||||
|
||||
String transactionId = x.genId().asString();
|
||||
|
||||
AccountChangeInfo change = new AccountChangeInfo(changeId, transactionId, AccountCreditedEvent.class.getSimpleName(),
|
||||
500, +1);
|
||||
|
||||
accountInfoUpdateService.updateBalance(accountId, changeId, 500,
|
||||
change);
|
||||
|
||||
accountInfo = accountQueryService.findByAccountId(accountId);
|
||||
assertEquals(initialBalance.add(new BigDecimal(5)).longValue() * 100, accountInfo.getBalance());
|
||||
assertFalse(accountInfo.getChanges().isEmpty());
|
||||
|
||||
assertEquals(change, accountInfo.getChanges().get(1));
|
||||
|
||||
String eventId = x.genId().asString();
|
||||
|
||||
AccountTransactionInfo ti = new AccountTransactionInfo(transactionId, accountId, accountId, 5, new Date(), "A transfer");
|
||||
|
||||
accountInfoUpdateService.addTransaction(accountId, ti);
|
||||
|
||||
accountInfo = accountQueryService.findByAccountId(accountId);
|
||||
assertFalse(accountInfo.getTransactions().isEmpty());
|
||||
|
||||
assertEquals(ti, accountInfo.getTransactions().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleDuplicateSaveAccountInfo() throws ExecutionException, InterruptedException {
|
||||
IdGenerator x = new IdGeneratorImpl();
|
||||
String accountId = x.genId().asString();
|
||||
String customerId = x.genId().asString();
|
||||
String version = x.genId().asString();
|
||||
|
||||
String title = "Checking account";
|
||||
BigDecimal initialBalance = new BigDecimal("1345");
|
||||
String description = "Some account";
|
||||
|
||||
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
|
||||
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldUpdateTransactionStatus() {
|
||||
IdGenerator x = new IdGeneratorImpl();
|
||||
String accountId = x.genId().asString();
|
||||
String customerId = x.genId().asString();
|
||||
String version = x.genId().asString();
|
||||
|
||||
String title = "Checking account";
|
||||
BigDecimal initialBalance = new BigDecimal("1345");
|
||||
String description = "Some account";
|
||||
|
||||
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
|
||||
|
||||
String transactionId = x.genId().asString();
|
||||
|
||||
AccountTransactionInfo transactionInfo = new AccountTransactionInfo();
|
||||
transactionInfo.setTransactionId(transactionId);
|
||||
transactionInfo.setStatus(TransferState.INITIAL);
|
||||
|
||||
accountInfoUpdateService.addTransaction(accountId, transactionInfo);
|
||||
|
||||
AccountInfo accountInfo = accountQueryService.findByAccountId(accountId);
|
||||
assertEquals(accountId, accountInfo.getId());
|
||||
assertFalse(accountInfo.getTransactions().isEmpty());
|
||||
assertEquals(1, accountInfo.getTransactions().size());
|
||||
|
||||
assertEquals(TransferState.INITIAL, accountInfo.getTransactions().get(0).getStatus());
|
||||
|
||||
accountInfoUpdateService.updateTransactionStatus(accountId, transactionId, TransferState.COMPLETED);
|
||||
|
||||
accountInfo = accountQueryService.findByAccountId(accountId);
|
||||
assertEquals(accountId, accountInfo.getId());
|
||||
assertFalse(accountInfo.getTransactions().isEmpty());
|
||||
assertEquals(1, accountInfo.getTransactions().size());
|
||||
|
||||
assertEquals(TransferState.COMPLETED, accountInfo.getTransactions().get(0).getStatus());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
apply plugin: VerifyMongoDBConfigurationPlugin
|
||||
apply plugin: VerifyEventStoreEnvironmentPlugin
|
||||
apply plugin: EventuateDependencyPlugin
|
||||
|
||||
apply plugin: 'spring-boot'
|
||||
|
||||
@@ -10,8 +11,6 @@ dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-web"
|
||||
compile "org.springframework.boot:spring-boot-starter-actuator"
|
||||
|
||||
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
|
||||
|
||||
testCompile project(":testutil")
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test"
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class AccountsQuerySideServiceIntegrationTest {
|
||||
private int port;
|
||||
|
||||
private String baseUrl(String path) {
|
||||
return "http://localhost:" + port + "/" + path;
|
||||
return "http://localhost:" + port + "/api" + path;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -4,18 +4,19 @@ import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.acc
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountNotFoundException;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountQueryService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.GetAccountResponse;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class AccountQueryController {
|
||||
|
||||
private AccountQueryService accountInfoQueryService;
|
||||
@@ -26,21 +27,35 @@ public class AccountQueryController {
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/accounts/{accountId}", method = RequestMethod.GET)
|
||||
public CompletableFuture<GetAccountResponse> get(@PathVariable String accountId) {
|
||||
return accountInfoQueryService.findByAccountId(accountId)
|
||||
.thenApply(accountInfo -> new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance()), accountInfo.getTitle(), accountInfo.getDescription()));
|
||||
public ResponseEntity<GetAccountResponse> get(@PathVariable String accountId) {
|
||||
AccountInfo accountInfo = accountInfoQueryService.findByAccountId(accountId);
|
||||
return ResponseEntity.ok().body(new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance()), accountInfo.getTitle(), accountInfo.getDescription()));
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/accounts", method = RequestMethod.GET)
|
||||
public CompletableFuture<List<GetAccountResponse>> getAccountsForCustomer(@RequestParam("customerId") String customerId) {
|
||||
return accountInfoQueryService.findByCustomerId(customerId)
|
||||
.thenApply(accountInfoList -> accountInfoList.stream().map(accountInfo -> new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance()), accountInfo.getTitle(), accountInfo.getDescription())).collect(Collectors.toList()));
|
||||
@RequestMapping(value = "/customers/{customerId}/accounts", method = RequestMethod.GET)
|
||||
public ResponseEntity<GetAccountsResponse> getAccountsForCustomer(@PathVariable("customerId") String customerId) {
|
||||
return ResponseEntity.ok().body(
|
||||
new GetAccountsResponse(
|
||||
accountInfoQueryService.findByCustomerId(customerId)
|
||||
.stream()
|
||||
.map(accountInfo -> new GetAccountResponse(
|
||||
accountInfo.getId(),
|
||||
new BigDecimal(accountInfo.getBalance()),
|
||||
accountInfo.getTitle(),
|
||||
accountInfo.getDescription()))
|
||||
.collect(Collectors.toList())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/accounts/{accountId}/history", method = RequestMethod.GET)
|
||||
public CompletableFuture<List<AccountTransactionInfo>> getTransactionsHistory(@PathVariable String accountId) {
|
||||
return accountInfoQueryService.findByAccountId(accountId)
|
||||
.thenApply(AccountInfo::getTransactions);
|
||||
public ResponseEntity<AccountHistoryResponse> getTransactionsHistory(@PathVariable String accountId) {
|
||||
AccountInfo accountInfo = accountInfoQueryService.findByAccountId(accountId);
|
||||
List<AccountHistoryEntry> historyEntries = new ArrayList<>();
|
||||
historyEntries.add(new AccountOpenInfo(accountInfo.getDate(), AccountHistoryEntry.EntryType.account, accountInfo.getChanges().get(0).getAmount()));
|
||||
accountInfo.getTransactions().forEach(historyEntries::add);
|
||||
|
||||
return ResponseEntity.ok().body(new AccountHistoryResponse(historyEntries));
|
||||
}
|
||||
|
||||
@ResponseStatus(value= HttpStatus.NOT_FOUND, reason="account not found")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'spring-boot'
|
||||
apply plugin: EventuateDependencyPlugin
|
||||
|
||||
dependencies {
|
||||
compile project(":common-auth-web")
|
||||
@@ -14,7 +15,7 @@ dependencies {
|
||||
}
|
||||
|
||||
task copyWebStatic(type: Copy) {
|
||||
from "../../prebuilt-web-client"
|
||||
from "../../js-frontend/build"
|
||||
into "build/resources/main/static"
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.ApiGateway
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.ContentRequestTransformer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.HeadersRequestTransformer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.URLRequestTransformer;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
@@ -12,9 +13,12 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
|
||||
|
||||
@@ -26,6 +30,7 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.springframework.web.bind.annotation.RequestMethod.GET;
|
||||
import static org.springframework.web.bind.annotation.RequestMethod.POST;
|
||||
@@ -52,13 +57,20 @@ public class GatewayController {
|
||||
.build();
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/**", method = {GET, POST})
|
||||
@RequestMapping(value = "/api/**", method = {GET, POST})
|
||||
@ResponseBody
|
||||
public ResponseEntity<String> proxyRequest(HttpServletRequest request) throws NoSuchRequestHandlingMethodException, IOException, URISyntaxException {
|
||||
HttpUriRequest proxiedRequest = createHttpUriRequest(request);
|
||||
logger.info("request: {}", proxiedRequest);
|
||||
HttpResponse proxiedResponse = httpClient.execute(proxiedRequest);
|
||||
logger.info("Response {}", proxiedResponse.getStatusLine().getStatusCode());
|
||||
return new ResponseEntity<>(read(proxiedResponse.getEntity().getContent()), HttpStatus.valueOf(proxiedResponse.getStatusLine().getStatusCode()));
|
||||
return new ResponseEntity<>(read(proxiedResponse.getEntity().getContent()), processHeaders(proxiedResponse.getAllHeaders()), HttpStatus.valueOf(proxiedResponse.getStatusLine().getStatusCode()));
|
||||
}
|
||||
|
||||
private HttpHeaders processHeaders(Header[] headers) {
|
||||
HttpHeaders result = new HttpHeaders();
|
||||
Stream.of(headers).filter(h -> h.getName().equalsIgnoreCase("Content-Type")).forEach( h -> result.set(h.getName(), h.getValue()));
|
||||
return result;
|
||||
}
|
||||
|
||||
private HttpUriRequest createHttpUriRequest(HttpServletRequest request) throws URISyntaxException, NoSuchRequestHandlingMethodException, IOException {
|
||||
|
||||
@@ -5,18 +5,21 @@ customers.queryside.service.host=localhost
|
||||
transfers.commandside.service.host=localhost
|
||||
|
||||
|
||||
api.gateway.endpoints[0].path=[/]*accounts.*
|
||||
api.gateway.endpoints[0].path=[/]*api/accounts.*
|
||||
api.gateway.endpoints[0].method=GET
|
||||
api.gateway.endpoints[0].location=http://${accounts.queryside.service.host}:8080
|
||||
api.gateway.endpoints[1].path=[/]*accounts.*
|
||||
api.gateway.endpoints[1].method=POST
|
||||
api.gateway.endpoints[1].location=http://${accounts.commandside.service.host}:8080
|
||||
api.gateway.endpoints[2].path=[/]*customers.*
|
||||
api.gateway.endpoints[2].method=GET
|
||||
api.gateway.endpoints[2].location=http://${customers.queryside.service.host}:8080
|
||||
api.gateway.endpoints[3].path=[/]*customers.*
|
||||
api.gateway.endpoints[3].method=POST
|
||||
api.gateway.endpoints[3].location=http://${customers.commandside.service.host}:8080
|
||||
api.gateway.endpoints[4].path=[/]*transfers.*
|
||||
api.gateway.endpoints[1].path=[/]*api/customers.*/accounts
|
||||
api.gateway.endpoints[1].method=GET
|
||||
api.gateway.endpoints[1].location=http://${accounts.queryside.service.host}:8080
|
||||
api.gateway.endpoints[2].path=[/]*api/accounts.*
|
||||
api.gateway.endpoints[2].method=POST
|
||||
api.gateway.endpoints[2].location=http://${accounts.commandside.service.host}:8080
|
||||
api.gateway.endpoints[3].path=[/]*api/customers.*
|
||||
api.gateway.endpoints[3].method=GET
|
||||
api.gateway.endpoints[3].location=http://${customers.queryside.service.host}:8080
|
||||
api.gateway.endpoints[4].path=[/]*api/customers.*
|
||||
api.gateway.endpoints[4].method=POST
|
||||
api.gateway.endpoints[4].location=http://${transfers.commandside.service.host}:8080
|
||||
api.gateway.endpoints[4].location=http://${customers.commandside.service.host}:8080
|
||||
api.gateway.endpoints[5].path=[/]*api/transfers.*
|
||||
api.gateway.endpoints[5].method=POST
|
||||
api.gateway.endpoints[5].location=http://${transfers.commandside.service.host}:8080
|
||||
@@ -3,6 +3,7 @@ package net.chrisrichardson.eventstore.javaexamples.banking.backend;
|
||||
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
|
||||
@@ -6,7 +6,7 @@ import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.a
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferState;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.TransferDetails;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -6,7 +6,7 @@ import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.a
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferState;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.TransferDetails;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -17,6 +17,7 @@ import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.await;
|
||||
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.eventually;
|
||||
@@ -55,10 +56,10 @@ public class AccountQuerySideIntegrationTest {
|
||||
updatedTransaction -> Assert.assertEquals(TransferState.COMPLETED, updatedTransaction.getEntity().getState()));
|
||||
|
||||
eventually(
|
||||
() -> accountQueryService.findByAccountId(fromAccount.getEntityId()),
|
||||
() -> CompletableFuture.completedFuture(accountQueryService.findByAccountId(fromAccount.getEntityId())),
|
||||
accountInfo -> Assert.assertEquals(70 * 100, accountInfo.getBalance()));
|
||||
eventually(
|
||||
() -> accountQueryService.findByAccountId(toAccount.getEntityId()),
|
||||
() -> CompletableFuture.completedFuture(accountQueryService.findByAccountId(toAccount.getEntityId())),
|
||||
accountInfo -> Assert.assertEquals(380 * 100, accountInfo.getBalance()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
|
||||
class EventuateDependencyPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
void apply(Project project) {
|
||||
project.dependencies {
|
||||
if (project.hasProperty("eventuateLocal")) {
|
||||
compile "io.eventuate.local.java:eventuate-local-java-jdbc:${project.eventuateLocalVersion}"
|
||||
compile "io.eventuate.local.java:eventuate-local-java-embedded-cdc-autoconfigure:${project.eventuateLocalVersion}"
|
||||
} else
|
||||
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:${project.eventuateClientVersion}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST;
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@RequestMapping("/api")
|
||||
public class AuthController {
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -80,7 +80,7 @@ public class AuthConfiguration extends WebSecurityConfigurerAdapter {
|
||||
.authorizeRequests()
|
||||
.antMatchers("/index.html", "/", "/**.js", "/**.css").permitAll()
|
||||
.antMatchers("/swagger-ui.html", "/v2/api-docs").permitAll()
|
||||
.antMatchers(HttpMethod.POST, "/customers", "/login").permitAll()
|
||||
.antMatchers(HttpMethod.POST, "/api/customers", "/api/login").permitAll()
|
||||
.anyRequest().authenticated().and()
|
||||
.addFilterAfter(new StatelessAuthenticationFilter(tokenAuthenticationService), BasicAuthenticationFilter.class);
|
||||
}
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
<logger name="org.springframework" level='info'>
|
||||
</logger>
|
||||
|
||||
<logger name="net.chrisrichardson.eventstore.javaexamples.banking" level='info'>
|
||||
</logger>
|
||||
|
||||
<logger name="io.eventuate" level='debug'>
|
||||
</logger>
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
|
||||
|
||||
import org.apache.commons.lang.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang.builder.HashCodeBuilder;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class AccountChangeInfo {
|
||||
|
||||
private String changeId;
|
||||
private String transactionId;
|
||||
private String transactionType;
|
||||
private long amount;
|
||||
private long balanceDelta;
|
||||
|
||||
public AccountChangeInfo() {
|
||||
}
|
||||
|
||||
public AccountChangeInfo(String changeId, String transactionId, String transactionType, long amount, long balanceDelta) {
|
||||
this(new Date(), changeId, transactionId, transactionType, amount, balanceDelta);
|
||||
}
|
||||
|
||||
public AccountChangeInfo(Date date, String changeId, String transactionId, String transactionType, long amount, long balanceDelta) {
|
||||
this.changeId = changeId;
|
||||
this.transactionId = transactionId;
|
||||
this.transactionType = transactionType;
|
||||
this.amount = amount;
|
||||
this.balanceDelta = balanceDelta;
|
||||
}
|
||||
|
||||
public String getChangeId() {
|
||||
return changeId;
|
||||
}
|
||||
|
||||
public void setChangeId(String changeId) {
|
||||
this.changeId = changeId;
|
||||
}
|
||||
|
||||
public String getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
public void setTransactionId(String transactionId) {
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
public String getTransactionType() {
|
||||
return transactionType;
|
||||
}
|
||||
|
||||
public void setTransactionType(String transactionType) {
|
||||
this.transactionType = transactionType;
|
||||
}
|
||||
|
||||
public long getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(long amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public long getBalanceDelta() {
|
||||
return balanceDelta;
|
||||
}
|
||||
|
||||
public void setBalanceDelta(long balanceDelta) {
|
||||
this.balanceDelta = balanceDelta;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return EqualsBuilder.reflectionEquals(this, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return HashCodeBuilder.reflectionHashCode(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Created by popikyardo on 9/1/16.
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS,
|
||||
include = JsonTypeInfo.As.PROPERTY,
|
||||
property = "entryType")
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = AccountTransactionInfo.class, name = "transaction"),
|
||||
@JsonSubTypes.Type(value = AccountOpenInfo.class, name = "account")
|
||||
})
|
||||
public class AccountHistoryEntry {
|
||||
|
||||
protected Date date;
|
||||
protected EntryType entryType;
|
||||
|
||||
public AccountHistoryEntry() {
|
||||
}
|
||||
|
||||
public AccountHistoryEntry(Date date, EntryType entryType) {
|
||||
this.date = date;
|
||||
this.entryType = entryType;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(Date date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public EntryType getEntryType() {
|
||||
return entryType;
|
||||
}
|
||||
|
||||
public void setEntryType(EntryType entryType) {
|
||||
this.entryType = entryType;
|
||||
}
|
||||
|
||||
public enum EntryType {
|
||||
transaction, account
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by popikyardo on 9/1/16.
|
||||
*/
|
||||
public class AccountHistoryResponse {
|
||||
private List<AccountHistoryEntry> transactionsHistory;
|
||||
|
||||
public AccountHistoryResponse() {
|
||||
}
|
||||
|
||||
public AccountHistoryResponse(List<AccountHistoryEntry> transactionsHistory) {
|
||||
|
||||
this.transactionsHistory = transactionsHistory;
|
||||
}
|
||||
|
||||
public List<AccountHistoryEntry> getTransactionsHistory() {
|
||||
return transactionsHistory;
|
||||
}
|
||||
|
||||
public void setTransactionsHistory(List<AccountHistoryEntry> transactionsHistory) {
|
||||
this.transactionsHistory = transactionsHistory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Created by popikyardo on 9/1/16.
|
||||
*/
|
||||
public class AccountOpenInfo extends AccountHistoryEntry {
|
||||
|
||||
private long initialBalance;
|
||||
|
||||
public AccountOpenInfo() {
|
||||
}
|
||||
|
||||
public AccountOpenInfo(Date date, EntryType entryType, long initialBalance) {
|
||||
super(date, entryType);
|
||||
this.initialBalance=initialBalance;
|
||||
}
|
||||
|
||||
public long getInitialBalance() {
|
||||
return initialBalance;
|
||||
}
|
||||
|
||||
public void setInitialBalance(long initialBalance) {
|
||||
this.initialBalance = initialBalance;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
import org.apache.commons.lang.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang.builder.HashCodeBuilder;
|
||||
import org.apache.commons.lang.builder.ToStringBuilder;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class AccountTransactionInfo {
|
||||
public class AccountTransactionInfo extends AccountHistoryEntry{
|
||||
|
||||
private String transactionId;
|
||||
private String fromAccountId;
|
||||
private String toAccountId;
|
||||
private long amount;
|
||||
private Date date;
|
||||
private String description;
|
||||
private TransferState status = TransferState.INITIAL;
|
||||
|
||||
public AccountTransactionInfo() {
|
||||
}
|
||||
@@ -21,6 +23,12 @@ public class AccountTransactionInfo {
|
||||
this(transactionId, fromAccountId, toAccountId, amount, new Date(), "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ToStringBuilder.reflectionToString(this);
|
||||
}
|
||||
|
||||
|
||||
public AccountTransactionInfo(String transactionId, String fromAccountId, String toAccountId, long amount, Date date, String description) {
|
||||
this.transactionId = transactionId;
|
||||
this.fromAccountId = fromAccountId;
|
||||
@@ -28,6 +36,7 @@ public class AccountTransactionInfo {
|
||||
this.amount = amount;
|
||||
this.date = date;
|
||||
this.description = description;
|
||||
this.entryType = EntryType.transaction;
|
||||
}
|
||||
|
||||
public String getTransactionId() {
|
||||
@@ -62,14 +71,6 @@ public class AccountTransactionInfo {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(Date date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
@@ -78,6 +79,14 @@ public class AccountTransactionInfo {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public TransferState getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(TransferState status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return EqualsBuilder.reflectionEquals(this, o);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by popikyardo on 9/1/16.
|
||||
*/
|
||||
public class GetAccountsResponse {
|
||||
private List<GetAccountResponse> accounts;
|
||||
|
||||
public GetAccountsResponse() {
|
||||
}
|
||||
|
||||
public GetAccountsResponse(List<GetAccountResponse> accounts) {
|
||||
this.accounts = accounts;
|
||||
}
|
||||
|
||||
public List<GetAccountResponse> getAccounts() {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
public void setAccounts(List<GetAccountResponse> accounts) {
|
||||
this.accounts = accounts;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.common.transactions;
|
||||
|
||||
public enum TransferState {
|
||||
NEW, INITIAL, DEBITED, COMPLETED, FAILED_DUE_TO_INSUFFICIENT_FUNDS
|
||||
@@ -4,10 +4,12 @@ import io.eventuate.AggregateRepository;
|
||||
import io.eventuate.EventuateAggregateStore;
|
||||
import io.eventuate.javaclient.spring.EnableEventHandlers;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableEventHandlers
|
||||
@ComponentScan
|
||||
public class CustomerConfiguration {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -2,6 +2,7 @@ apply plugin: VerifyMongoDBConfigurationPlugin
|
||||
apply plugin: VerifyEventStoreEnvironmentPlugin
|
||||
|
||||
apply plugin: 'spring-boot'
|
||||
apply plugin: EventuateDependencyPlugin
|
||||
|
||||
dependencies {
|
||||
compile project(":customers-command-side-web")
|
||||
@@ -10,8 +11,7 @@ dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-web"
|
||||
compile "org.springframework.boot:spring-boot-starter-actuator"
|
||||
|
||||
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
|
||||
|
||||
testCompile project(":testutil")
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test"
|
||||
testCompile "io.eventuate.client.java:eventuate-client-java-jdbc:$eventuateClientVersion"
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.custo
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@@ -15,7 +14,6 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
|
||||
@Configuration
|
||||
@Import({CustomersCommandSideWebConfiguration.class, EventuateHttpStompClientConfiguration.class, CommonSwaggerConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan
|
||||
public class CustomersCommandSideServiceConfiguration {
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ public class CustomersCommandSideServiceIntegrationTest {
|
||||
private int port;
|
||||
|
||||
private String baseUrl(String path) {
|
||||
return "http://localhost:" + port + "/" + path;
|
||||
return "http://localhost:" + port + "/api" + path;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web;
|
||||
|
||||
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.AuthConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.customers.CustomersCommandSideWebConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -13,9 +17,16 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@Import({CustomersCommandSideServiceConfiguration.class, AuthConfiguration.class})
|
||||
@Import({CustomersCommandSideWebConfiguration.class, EventuateJdbcEventStoreConfiguration.class, CommonSwaggerConfiguration.class, AuthConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
public class CustomersCommandSideServiceTestConfiguration {
|
||||
|
||||
@Bean
|
||||
public HttpMessageConverters customConverters() {
|
||||
HttpMessageConverter<?> additional = new MappingJackson2HttpMessageConverter();
|
||||
return new HttpMessageConverters(additional);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate(HttpMessageConverters converters) {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
* Created by popikyardo on 03.02.16.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/customers")
|
||||
@RequestMapping("/api/customers")
|
||||
public class CustomerController {
|
||||
|
||||
private CustomerService customerService;
|
||||
|
||||
@@ -5,6 +5,7 @@ import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.Quer
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.ToAccountInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@@ -13,36 +14,38 @@ import java.util.Collections;
|
||||
*/
|
||||
public class CustomerInfoUpdateService {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private QuerySideCustomerRepository accountInfoRepository;
|
||||
private QuerySideCustomerRepository querySideCustomerRepository;
|
||||
|
||||
public CustomerInfoUpdateService(QuerySideCustomerRepository accountInfoRepository) {
|
||||
this.accountInfoRepository = accountInfoRepository;
|
||||
}
|
||||
|
||||
public void create(String id, CustomerInfo customerInfo) {
|
||||
try {
|
||||
accountInfoRepository.save(new QuerySideCustomer(id,
|
||||
customerInfo.getName(),
|
||||
customerInfo.getEmail(),
|
||||
customerInfo.getSsn(),
|
||||
customerInfo.getPhoneNumber(),
|
||||
customerInfo.getAddress(),
|
||||
Collections.<String, ToAccountInfo>emptyMap()
|
||||
)
|
||||
);
|
||||
logger.info("Saved in mongo");
|
||||
} catch (Throwable t) {
|
||||
logger.error("Error during saving: ", t);
|
||||
throw new RuntimeException(t);
|
||||
public CustomerInfoUpdateService(QuerySideCustomerRepository querySideCustomerRepository) {
|
||||
this.querySideCustomerRepository = querySideCustomerRepository;
|
||||
}
|
||||
}
|
||||
|
||||
public void addToAccount(String id, ToAccountInfo accountInfo) {
|
||||
QuerySideCustomer customer = accountInfoRepository.findOne(id);
|
||||
customer.getToAccounts().put(accountInfo.getId(), accountInfo);
|
||||
accountInfoRepository.save(customer);
|
||||
}
|
||||
public void create(String id, CustomerInfo customerInfo) {
|
||||
try {
|
||||
querySideCustomerRepository.save(new QuerySideCustomer(id,
|
||||
customerInfo.getName(),
|
||||
customerInfo.getEmail(),
|
||||
customerInfo.getSsn(),
|
||||
customerInfo.getPhoneNumber(),
|
||||
customerInfo.getAddress(),
|
||||
Collections.<String, ToAccountInfo>emptyMap()
|
||||
)
|
||||
);
|
||||
logger.info("Saved in mongo");
|
||||
} catch (DuplicateKeyException t) {
|
||||
logger.warn("When saving ", t);
|
||||
} catch (Throwable t) {
|
||||
logger.error("Error during saving: ", t);
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}
|
||||
|
||||
public void addToAccount(String id, ToAccountInfo accountInfo) {
|
||||
QuerySideCustomer customer = querySideCustomerRepository.findOne(id);
|
||||
customer.getToAccounts().put(accountInfo.getId(), accountInfo);
|
||||
querySideCustomerRepository.save(customer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.cu
|
||||
|
||||
import io.eventuate.javaclient.spring.EnableEventHandlers;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
|
||||
@@ -12,6 +13,7 @@ import org.springframework.data.mongodb.repository.config.EnableMongoRepositorie
|
||||
@Configuration
|
||||
@EnableMongoRepositories
|
||||
@EnableEventHandlers
|
||||
@ComponentScan
|
||||
public class QuerySideCustomerConfiguration {
|
||||
@Bean
|
||||
public CustomerQueryWorkflow customerQueryWorkflow(CustomerInfoUpdateService accountInfoUpdateService) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
apply plugin: VerifyMongoDBConfigurationPlugin
|
||||
apply plugin: VerifyEventStoreEnvironmentPlugin
|
||||
apply plugin: EventuateDependencyPlugin
|
||||
|
||||
apply plugin: 'spring-boot'
|
||||
|
||||
@@ -10,8 +11,6 @@ dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-web"
|
||||
compile "org.springframework.boot:spring-boot-starter-actuator"
|
||||
|
||||
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
|
||||
|
||||
testCompile project(":testutil")
|
||||
testCompile project(":customers-command-side-service")
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test"
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web;
|
||||
|
||||
import io.eventuate.javaclient.spring.httpstomp.EventuateHttpStompClientConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.customers.QuerySideCustomerConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.CustomersQuerySideWebConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
|
||||
@Configuration
|
||||
@Import({QuerySideCustomerConfiguration.class, EventuateHttpStompClientConfiguration.class, CommonSwaggerConfiguration.class})
|
||||
@Import({CustomersQuerySideWebConfiguration.class, EventuateHttpStompClientConfiguration.class, CommonSwaggerConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan
|
||||
public class CustomersQuerySideServiceConfiguration {
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.*;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerResponse;
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.CustomersTestUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -12,7 +12,6 @@ import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import rx.Observable;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
@@ -28,7 +27,7 @@ public class CustomersQuerySideServiceIntegrationTest {
|
||||
private int port;
|
||||
|
||||
private String baseUrl(String path) {
|
||||
return "http://localhost:" + port + "/" + path;
|
||||
return "http://localhost:" + port + "/api" + path;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.AuthConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -13,7 +14,8 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@Import({CustomersQuerySideServiceConfiguration.class, CustomersQuerySideServiceConfiguration.class, AuthConfiguration.class})
|
||||
@Import({CustomersCommandSideServiceConfiguration.class, CustomersQuerySideServiceConfiguration.class, AuthConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
public class CustomersQuerySideServiceTestConfiguration {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
* Created by Main on 05.02.2016.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class CustomerQueryController {
|
||||
|
||||
private CustomerQueryService customerQueryService;
|
||||
|
||||
129
java-spring/docker-compose-eventuate-local.yml
Normal file
129
java-spring/docker-compose-eventuate-local.yml
Normal file
@@ -0,0 +1,129 @@
|
||||
apigateway:
|
||||
image: java:openjdk-8u91-jdk
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./api-gateway-service/build/libs:/app
|
||||
command: java -jar /app/api-gateway-service.jar --accounts.commandside.service.host=accountscommandside --transfers.commandside.service.host=transactionscommandside --accounts.queryside.service.host=accountsqueryside --customers.commandside.service.host=customerscommandside --customers.queryside.service.host=customersqueryside
|
||||
ports:
|
||||
- "8080:8080"
|
||||
links:
|
||||
- accountscommandside
|
||||
- transactionscommandside
|
||||
- accountsqueryside
|
||||
- customerscommandside
|
||||
- customersqueryside
|
||||
- mongodb
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL:
|
||||
SPRING_DATASOURCE_USERNAME:
|
||||
SPRING_DATASOURCE_PASSWORD:
|
||||
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
|
||||
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
|
||||
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
|
||||
SPRING_DATA_MONGODB_URI: mongodb://mongodb/mydb
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
|
||||
|
||||
accountscommandside:
|
||||
image: java:openjdk-8u91-jdk
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./accounts-command-side-service/build/libs:/app
|
||||
command: java -jar /app/accounts-command-side-service.jar
|
||||
ports:
|
||||
- "8085:8080"
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL:
|
||||
SPRING_DATASOURCE_USERNAME:
|
||||
SPRING_DATASOURCE_PASSWORD:
|
||||
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
|
||||
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
|
||||
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
|
||||
|
||||
transactionscommandside:
|
||||
image: java:openjdk-8u91-jdk
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./transactions-command-side-service/build/libs:/app
|
||||
command: java -jar /app/transactions-command-side-service.jar
|
||||
ports:
|
||||
- "8082:8080"
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL:
|
||||
SPRING_DATASOURCE_USERNAME:
|
||||
SPRING_DATASOURCE_PASSWORD:
|
||||
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
|
||||
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
|
||||
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
|
||||
|
||||
|
||||
accountsqueryside:
|
||||
image: java:openjdk-8u91-jdk
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./accounts-query-side-service/build/libs:/app
|
||||
command: java -jar /app/accounts-query-side-service.jar
|
||||
ports:
|
||||
- "8081:8080"
|
||||
links:
|
||||
- mongodb
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL:
|
||||
SPRING_DATASOURCE_USERNAME:
|
||||
SPRING_DATASOURCE_PASSWORD:
|
||||
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
|
||||
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
|
||||
SPRING_DATA_MONGODB_URI: mongodb://mongodb/mydb
|
||||
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
|
||||
|
||||
customerscommandside:
|
||||
image: java:openjdk-8u91-jdk
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./customers-command-side-service/build/libs:/app
|
||||
command: java -jar /app/customers-command-side-service.jar
|
||||
ports:
|
||||
- "8083:8080"
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL:
|
||||
SPRING_DATASOURCE_USERNAME:
|
||||
SPRING_DATASOURCE_PASSWORD:
|
||||
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
|
||||
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
|
||||
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
|
||||
|
||||
customersqueryside:
|
||||
image: java:openjdk-8u91-jdk
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./customers-query-side-service/build/libs:/app
|
||||
command: java -jar /app/customers-query-side-service.jar
|
||||
ports:
|
||||
- "8084:8080"
|
||||
links:
|
||||
- mongodb
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL:
|
||||
SPRING_DATASOURCE_USERNAME:
|
||||
SPRING_DATASOURCE_PASSWORD:
|
||||
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
|
||||
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
|
||||
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
|
||||
SPRING_DATA_MONGODB_URI: mongodb://mongodb/mydb
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
|
||||
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
|
||||
|
||||
mongodb:
|
||||
image: mongo:3.0.4
|
||||
hostname: mongodb
|
||||
command: mongod --smallfiles
|
||||
ports:
|
||||
- "27017:27017"
|
||||
@@ -18,7 +18,7 @@ public class EndToEndTest extends AbstractRestAPITest {
|
||||
CustomersTestUtils customersTestUtils = new CustomersTestUtils(restTemplate, baseUrl("/customers/"));
|
||||
|
||||
public String baseUrl(String path) {
|
||||
return "http://" + getenv("SERVICE_HOST", "localhost") + ":" + 8080 + "/" + path;
|
||||
return "http://" + getenv("SERVICE_HOST", "localhost") + ":" + 8080 + "/api" + path;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,4 +6,5 @@ eventuateMavenRepoUrl=http://mavenrepo.eventuate.io/release
|
||||
springBootVersion=1.3.5.RELEASE
|
||||
|
||||
eventuateClientVersion=0.8.0.RELEASE
|
||||
eventuateLocalVersion=0.2.0.RELEASE
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#! /bin/bash
|
||||
|
||||
docker run --link javaspring_mongodb_1:mongodb -i -t mongo:3.0.4 /usr/bin/mongo --host mongodb
|
||||
|
||||
docker run --rm --link javaspring_mongodb_1:mongodb -i -t mongo:3.0.4 /usr/bin/mongo --host mongodb
|
||||
|
||||
@@ -15,6 +15,7 @@ dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-actuator"
|
||||
|
||||
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
|
||||
compile project(":common-swagger")
|
||||
|
||||
testCompile project(":testutil")
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test"
|
||||
@@ -22,9 +23,9 @@ dependencies {
|
||||
}
|
||||
|
||||
task copyWebStatic(type: Copy) {
|
||||
from "../../prebuilt-web-client"
|
||||
from "../../js-frontend/build"
|
||||
into "build/resources/main/static"
|
||||
}
|
||||
|
||||
jar.dependsOn(copyWebStatic)
|
||||
bootRun.dependsOn(copyWebStatic)
|
||||
bootRun.dependsOn(copyWebStatic)
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
@@ -31,6 +32,12 @@ public class BankingWebConfiguration extends WebMvcConfigurerAdapter {
|
||||
return new HttpMessageConverters(additional);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
|
||||
System.setProperty("vertx.disableFileCPResolving", "true");
|
||||
return new PropertySourcesPlaceholderConfigurer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
registry.addViewController("/").setViewName("forward:/index.html");
|
||||
|
||||
@@ -43,7 +43,7 @@ public class BankingAuthTest {
|
||||
}
|
||||
|
||||
private String baseUrl(String path) {
|
||||
return "http://localhost:" + port + "/" + path;
|
||||
return "http://localhost:" + port + "/api" + path;
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -57,6 +57,12 @@ public class BankingAuthTest {
|
||||
Assert.assertNotNull(customerId);
|
||||
Assert.assertEquals(customerInfo, customerResponse.getCustomerInfo());
|
||||
|
||||
try {
|
||||
Thread.sleep(10000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
customersTestUtils.assertCustomerResponse(customerId, customerInfo);
|
||||
|
||||
AuthRequest authRequest = new AuthRequest(email);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web;
|
||||
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.AbstractRestAPITest;
|
||||
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.AuthenticatedRestTemplate;
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.CustomersTestUtils;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -11,7 +12,6 @@ import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@@ -32,7 +32,7 @@ public class BankingWebIntegrationTest extends AbstractRestAPITest {
|
||||
|
||||
@Override
|
||||
public String baseUrl(String path) {
|
||||
return "http://localhost:" + port + "/" + path;
|
||||
return "http://localhost:" + port + "/api" + path;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
7
java-spring/show-urls.sh
Executable file
7
java-spring/show-urls.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
IP=$(docker-machine ip default)
|
||||
|
||||
echo Accounts command-side service = http://${IP}:8080/swagger-ui.html
|
||||
echo Money Transfers command-side service = http://${IP}:8082/swagger-ui.html
|
||||
echo Accounts query-side service = http://${IP}:8081/swagger-ui.html
|
||||
@@ -1,9 +1,6 @@
|
||||
package net.chrisrichardson.eventstorestore.javaexamples.testutil;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountRequest;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountResponse;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.GetAccountResponse;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.*;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerResponse;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.QuerySideCustomer;
|
||||
@@ -62,21 +59,21 @@ public abstract class AbstractRestAPITest {
|
||||
assertAccountBalance(toAccountId, finalToAccountBalance);
|
||||
|
||||
eventually(
|
||||
new Producer<AccountTransactionInfo[]>() {
|
||||
new Producer<AccountHistoryResponse>() {
|
||||
@Override
|
||||
public CompletableFuture<AccountTransactionInfo[]> produce() {
|
||||
public CompletableFuture<AccountHistoryResponse> produce() {
|
||||
return CompletableFuture.completedFuture(getAuthenticatedRestTemplate().getForEntity(baseUrl("/accounts/" + fromAccountId + "/history"),
|
||||
AccountTransactionInfo[].class));
|
||||
AccountHistoryResponse.class));
|
||||
}
|
||||
},
|
||||
new Verifier<AccountTransactionInfo[]>() {
|
||||
new Verifier<AccountHistoryResponse>() {
|
||||
@Override
|
||||
public void verify(AccountTransactionInfo[] transactionInfos) {
|
||||
Optional<AccountTransactionInfo> first = Arrays.asList(transactionInfos).stream().filter(ti -> ti.getTransactionId().equals(moneyTransfer.getMoneyTransferId())).findFirst();
|
||||
public void verify(AccountHistoryResponse accountHistoryResponse) {
|
||||
Optional<AccountHistoryEntry> first = accountHistoryResponse.getTransactionsHistory().stream().filter( ahe -> ahe.getEntryType() == AccountHistoryEntry.EntryType.transaction && ((AccountTransactionInfo)ahe).getTransactionId().equals(moneyTransfer.getMoneyTransferId())).findFirst();
|
||||
|
||||
assertTrue(first.isPresent());
|
||||
|
||||
AccountTransactionInfo ti = first.get();
|
||||
AccountTransactionInfo ti = (AccountTransactionInfo)first.get();
|
||||
|
||||
assertEquals(fromAccountId, ti.getFromAccountId());
|
||||
assertEquals(toAccountId, ti.getToAccountId());
|
||||
@@ -111,17 +108,17 @@ public abstract class AbstractRestAPITest {
|
||||
assertAccountBalance(accountId, initialFromAccountBalance);
|
||||
|
||||
eventually(
|
||||
new Producer<GetAccountResponse[]>() {
|
||||
new Producer<GetAccountsResponse>() {
|
||||
@Override
|
||||
public CompletableFuture<GetAccountResponse[]> produce() {
|
||||
return CompletableFuture.completedFuture(getAuthenticatedRestTemplate().getForEntity(baseUrl("/accounts?customerId=" + customerId),
|
||||
GetAccountResponse[].class));
|
||||
public CompletableFuture<GetAccountsResponse> produce() {
|
||||
return CompletableFuture.completedFuture(getAuthenticatedRestTemplate().getForEntity(baseUrl("/customers/"+customerId+"/accounts"),
|
||||
GetAccountsResponse.class));
|
||||
}
|
||||
},
|
||||
new Verifier<GetAccountResponse[]>() {
|
||||
new Verifier<GetAccountsResponse>() {
|
||||
@Override
|
||||
public void verify(GetAccountResponse[] accountResponses) {
|
||||
assertTrue(Arrays.asList(accountResponses).stream().filter(acc -> acc.getAccountId().equals(accountId)).findFirst().isPresent());
|
||||
public void verify(GetAccountsResponse accountResponses) {
|
||||
assertTrue(accountResponses.getAccounts().stream().filter(acc -> acc.getAccountId().equals(accountId)).findFirst().isPresent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package net.chrisrichardson.eventstorestore.javaexamples.testutil;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.utils.BasicAuthUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.utils;
|
||||
package net.chrisrichardson.eventstorestore.javaexamples.testutil;
|
||||
|
||||
import org.apache.tomcat.util.codec.binary.Base64;
|
||||
import org.springframework.http.*;
|
||||
@@ -37,7 +37,7 @@ public class TestUtil {
|
||||
|
||||
}
|
||||
|
||||
static class Success<T> implements Outcome<T> {
|
||||
static class Success<T> implements Outcome<T> {
|
||||
|
||||
T value;
|
||||
|
||||
@@ -54,51 +54,26 @@ public class TestUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> void eventually(final Producer<T> producer, final Verifier<T> verifier) {
|
||||
final int n = 50;
|
||||
Object possibleException = Observable.timer(0, 200, TimeUnit.MILLISECONDS).flatMap(new Func1<Long, Observable<Outcome<T>>>() {
|
||||
|
||||
@Override
|
||||
public Observable<Outcome<T>> call(Long aLong) {
|
||||
try {
|
||||
return fromCompletableFuture(producer.produce()).map(new Func1<T, Outcome<T>>() {
|
||||
@Override
|
||||
public Outcome<T> call(T t) {
|
||||
return new Success<T>(t);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Outcome<T> value = new Failure<T>(e);
|
||||
return Observable.just(value);
|
||||
}
|
||||
public static <T> void eventually(Producer<T> producer, Verifier<T> predicate) {
|
||||
Throwable laste = null;
|
||||
for (int i = 0; i < 30 ; i++) {
|
||||
try {
|
||||
T x = producer.produce().get(30, TimeUnit.SECONDS);
|
||||
predicate.verify(x);
|
||||
return;
|
||||
} catch (Throwable t) {
|
||||
laste = t;
|
||||
}
|
||||
}).map(new Func1<Outcome<T>, Throwable>() {
|
||||
@Override
|
||||
public Throwable call(Outcome<T> t) {
|
||||
try {
|
||||
if (t instanceof Success) {
|
||||
verifier.verify(((Success<T>) t).value);
|
||||
return null;
|
||||
} else
|
||||
return ((Failure<T>) t).t;
|
||||
} catch (Throwable e) {
|
||||
return e;
|
||||
}
|
||||
try {
|
||||
TimeUnit.SECONDS.sleep(1);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}).take(n).zipWith(Observable.range(0, n), new Func2<Throwable, Integer, Tuple2<Throwable, Integer>>() {
|
||||
@Override
|
||||
public Tuple2<Throwable, Integer> call(Throwable e, Integer idx) {
|
||||
return new Tuple2<Throwable, Integer>(e, idx);
|
||||
}
|
||||
}).skipWhile(new Func1<Tuple2<Throwable, Integer>, Boolean>() {
|
||||
@Override
|
||||
public Boolean call(Tuple2<Throwable, Integer> tuple2) {
|
||||
return tuple2.first != null && tuple2.second < n - 1;
|
||||
}
|
||||
}).first().toBlocking().getIterator().next().first;
|
||||
|
||||
if (possibleException != null)
|
||||
throw new RuntimeException((Throwable) possibleException);
|
||||
}
|
||||
if (laste != null)
|
||||
throw new RuntimeException("Last exception was", laste);
|
||||
else
|
||||
throw new RuntimeException("predicate never satisfied");
|
||||
}
|
||||
|
||||
private static <T> Observable<T> fromCompletableFuture(CompletableFuture<T> future) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.eventuate.Event;
|
||||
import io.eventuate.EventUtil;
|
||||
import io.eventuate.ReflectiveMutableCommandProcessingAggregate;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.*;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
apply plugin: 'spring-boot'
|
||||
apply plugin: EventuateDependencyPlugin
|
||||
|
||||
apply plugin: VerifyEventStoreEnvironmentPlugin
|
||||
|
||||
@@ -9,8 +10,6 @@ dependencies {
|
||||
compile "org.springframework.boot:spring-boot-starter-web"
|
||||
compile "org.springframework.boot:spring-boot-starter-actuator"
|
||||
|
||||
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
|
||||
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test"
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public class TransactionsCommandSideServiceIntegrationTest {
|
||||
private int port;
|
||||
|
||||
private String baseUrl(String path) {
|
||||
return "http://localhost:" + port + "/" + path;
|
||||
return "http://localhost:" + port + "/api" + path;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.util.Date;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/transfers")
|
||||
@RequestMapping("/api/transfers")
|
||||
public class MoneyTransferController {
|
||||
|
||||
private final MoneyTransferService moneyTransferService;
|
||||
|
||||
@@ -36,7 +36,7 @@ public class MoneyTransferControllerIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void shouldCreateAccount() throws Exception {
|
||||
mockMvc.perform(post("/transfers")
|
||||
mockMvc.perform(post("/api/transfers")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"fromAccountId\" : \"fromAccountId\", \"toAccountId\" : \"toAccountId\", \"amount\" : \"500\"}")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
@@ -45,7 +45,7 @@ public class MoneyTransferControllerIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void shouldRejectBadRequest() throws Exception {
|
||||
mockMvc.perform(post("/transfers")
|
||||
mockMvc.perform(post("/api/transfers")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"fromAccountId\" : \"fromAccountIdXXXXXX\"}, {\"toAccountId\" : \"toAccountId\"}, {\"amount\" : \"500\"}")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"presets": ["es2015"]
|
||||
"presets": ["es2015", "stage-0", "react"]
|
||||
}
|
||||
|
||||
1
js-frontend/.gitignore
vendored
1
js-frontend/.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
node_modules
|
||||
build
|
||||
!/build
|
||||
#dist
|
||||
dist-intermediate
|
||||
|
||||
@@ -1,153 +1,15 @@
|
||||
# Unicorn Standard Starter Kit
|
||||
# Money Transfer App - Frontend Client
|
||||
|
||||
This starter kit provides you with the code and conventions you need to get straight into building your React/Redux based app.
|
||||
This..
|
||||
|
||||
## Happiness is six lines away
|
||||
|
||||
*Prerequisites: node.js and git*
|
||||
|
||||
```
|
||||
git clone https://github.com/unicorn-standard/starter-kit.git your-repo-name
|
||||
cd your-repo-name
|
||||
cd js-frontend
|
||||
npm install
|
||||
npm start
|
||||
npm run open # (from a different console window, otherwise open localhost:3000)
|
||||
npm run build
|
||||
```
|
||||
|
||||
Presto, you've got a ready-to-customise application!
|
||||
|
||||

|
||||
|
||||
## Why use Unicorn Standard Starter Kit?
|
||||
|
||||
- Your directory structure is sorted as soon as you `git clone`
|
||||
- ES6 compilation and automatic-reloading development server are configured for you with [webpack](https://webpack.github.io/) and [Babel](https://babeljs.io/)
|
||||
- [Redux](http://redux.js.org/) is an incredibly simple way of modelling your data, with great community support
|
||||
- [React](https://www.reactjs.org/) is an incredibly simple way of rendering your views, and is maintained by Facebook
|
||||
- Simple [uniloc](http://unicornstandard.com/packages/uniloc.html)-based routing is included - easy to understand, and easy to customize
|
||||
- The [Pacomo](http://unicornstandard.com/packages/pacomo.html) CSS conventions eliminate bugs caused by conflicting styles
|
||||
- The actors pattern allows you to easily react to changes on your store *without* forcing a re-render
|
||||
- Your redux store is already configured with navigation, data and view models
|
||||
- Comes with views, layouts and reducers for a simple document editor!
|
||||
|
||||
## Getting Started
|
||||
|
||||
#### Put your name on it
|
||||
|
||||
- Update name, desription and author in `package.json`
|
||||
- Update app title in `src/index.html`
|
||||
- Restart the dev server (make sure to do this after any changes to `src/index.html`)
|
||||
|
||||
#### Make sure your editor is happy
|
||||
|
||||
- Setup ES6 syntax highlighting on extensions `.js` and `.jsx` (see [babel-sublime](https://github.com/babel/babel-sublime))
|
||||
|
||||
#### Start building
|
||||
|
||||
- Add a route to `src/constants/ROUTES.js`
|
||||
- Add a nav menu item for your route in `src/components/ApplicationLayout.jsx`
|
||||
- Add a component for your route in `src/components`
|
||||
- Add reducers and actions for your component's view model in `src/actions` and `src/reducers/view`
|
||||
- Add any data models which your component reqiures in `src/reducers/data`
|
||||
- Add a container to map your store's `state` and `dispatch` to component props in `src/containers`
|
||||
- Configure your route in `src/Application.jsx`
|
||||
- Bask in the glory of your creation
|
||||
- Don't forget to commit your changes and push to Bitbucket or GitHub!
|
||||
|
||||
#### Show your friends
|
||||
|
||||
- Run `gulp dist` to output a web-ready build of your app to `dist`
|
||||
|
||||
## Structure
|
||||
|
||||
### Entry point
|
||||
|
||||
`main.js` is the entry point to your application. It defines your redux store, handles any actions dispatched to your redux store, handles changes to the browser's current URL, and also makes an initial route change dispatch.
|
||||
|
||||
Most of the above will be obvious from a quick read through `main.js` - if there is one thing which may strike you as "interesting", it'll be the block which handles actors.
|
||||
|
||||
### Actors
|
||||
|
||||
*[Read the introduction to actors](http://jamesknelson.com/join-the-dark-side-of-the-flux-responding-to-actions-with-actors/)*
|
||||
|
||||
Each time your store's state changes, a sequence of functions are called on the *current state* of your store. These functions are called **actors**.
|
||||
|
||||
There is one important exception to this rule: actors will not be called if `main.js` is currently in the midst of calling the sequence from a previous update. This allows earlier actors in a sequence to dispatch actions to the store, with later actors in the sequence receiving the *updated* state.
|
||||
|
||||
The code which accomplishes this is very small:
|
||||
|
||||
```javascript
|
||||
let acting = false
|
||||
store.subscribe(function() {
|
||||
// Ensure that any action dispatched by actors do not result in a new
|
||||
// actor run, allowing actors to dispatch with impunity.
|
||||
if (!acting) {
|
||||
acting = true
|
||||
|
||||
for (let actor of actors) {
|
||||
actor(store.getState(), store.dispatch.bind(store))
|
||||
}
|
||||
|
||||
acting = false
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
The actor is defined in `src/actors/index.js`. By default, it runs the following sequence:
|
||||
|
||||
- **redirector** - dispatch a navigation action if the current location should redirect to another location
|
||||
- **renderer** - renders your <Application> component with React
|
||||
|
||||
### Model
|
||||
|
||||
Your model (i.e. reducers and actions) is pre-configured with three parts:
|
||||
|
||||
#### Navigation
|
||||
|
||||
The `navigation` state holds the following information:
|
||||
|
||||
- `location` is the object which your `ROUTES` constant's `lookup` function returns for the current URL. With the default uniloc-based `ROUTES` object, this will have a string `name` property, and an `options` object containing any route parameters.
|
||||
- `transitioning` is true if a navigation `start` action has been dispatched, but the browser hasn't changed URL yet
|
||||
|
||||
#### Data
|
||||
|
||||
The `data` state can be thought of as the database for your application. If your application reads data from a remote server, it should be stored here. Any metadata should also be stored here, including the time it was fetched or its current version number.
|
||||
|
||||
#### View
|
||||
|
||||
The `view` state has a property for each of the view's in your app, holding their current state. For example, form state should be stored in the view models.
|
||||
|
||||
### Directories
|
||||
|
||||
- `src/actions` - Redux action creators
|
||||
- `src/actors` - Handle changes to your store's state
|
||||
- `src/components` - React components, stateless where possible
|
||||
- `src/constants` - Define stateless data
|
||||
- `src/containers` - Unstyled "smart" components which take your store's `state` and `dispatch`, and possibly navigation `location`, and pass them to "dumb" components
|
||||
- `src/reducers` - Redux reducers
|
||||
- `src/static` - Files which will be copied across to the root directory on build
|
||||
- `src/styles` - Helpers for stylesheets for individual components
|
||||
- `src/utils` - General code which isn't specific to your application
|
||||
- `src/validators` - Functions which take an object containing user entry and return an object containing any errors
|
||||
|
||||
Other directories:
|
||||
|
||||
- `build` - Intermediate files produced by the development server. Don't touch these.
|
||||
- `dist` - The output of `gulp dist`, which contains your distribution-ready app.
|
||||
- `config/environments` - The build system will assign one of these to the `environment` module, depending on the current build environment.
|
||||
|
||||
Main application files:
|
||||
|
||||
- `src/Application.jsx` - Your application's top-level React component
|
||||
- `src/index.html` - The single page for your single page application
|
||||
- `src/main.js` - The application's entry point
|
||||
- `src/main.less` - Global styles for your application
|
||||
|
||||
Main build files:
|
||||
|
||||
- `gulpfile.babel.js` - Build scripts written with [gulp](http://gulpjs.com/)
|
||||
- `webpack.config.js` - [Webpack](http://webpack.github.io/) configuration
|
||||
|
||||
## TODO
|
||||
|
||||
- Watch `static` and `index.html` for changes and copy them across to `build` when appropriate
|
||||
Text..
|
||||
|
||||
0
js-frontend/build/.gitkeep
Normal file
0
js-frontend/build/.gitkeep
Normal file
7356
js-frontend/build/app.d4bdff82ac1db214898b.js
Normal file
7356
js-frontend/build/app.d4bdff82ac1db214898b.js
Normal file
File diff suppressed because it is too large
Load Diff
1
js-frontend/build/app.d4bdff82ac1db214898b.js.map
Normal file
1
js-frontend/build/app.d4bdff82ac1db214898b.js.map
Normal file
File diff suppressed because one or more lines are too long
31
js-frontend/build/index.html
Normal file
31
js-frontend/build/index.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<!--[if lt IE 7 ]> <html lang="en" class="ie6" > <![endif]-->
|
||||
<!--[if IE 7 ]> <html lang="en" class="ie7" > <![endif]-->
|
||||
<!--[if IE 8 ]> <html lang="en" class="ie8" > <![endif]-->
|
||||
<!--[if IE 9 ]> <html lang="en" class="ie9" > <![endif]-->
|
||||
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="" > <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Money Transfer App</title><meta name="description" content="ES Money Transfer App" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" /><!-- Latest compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">
|
||||
|
||||
<!-- Optional theme -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap-theme.min.css"><link href="/style.6d7a32b1405ea1bb2bdf.css" rel="stylesheet"></head>
|
||||
<body><div id="root"></div><script src="/manifest.09cb8f5a05c9cfc35585.js"></script><script src="/vendor.f73c0104cb72cfb2809e.js"></script><script src="/style.6d7a32b1405ea1bb2bdf.js"></script><script src="/app.d4bdff82ac1db214898b.js"></script><script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
|
||||
|
||||
ga('create', 'UA-XXXX-XX', 'auto');
|
||||
|
||||
|
||||
|
||||
ga('send', 'pageview');
|
||||
|
||||
</script><!--{"files":{"publicPath":"/","chunks":{"manifest":{"size":0,"entry":"/manifest.09cb8f5a05c9cfc35585.js","hash":"09cb8f5a05c9cfc35585","css":[]},"vendor":{"size":1670874,"entry":"/vendor.f73c0104cb72cfb2809e.js","hash":"f73c0104cb72cfb2809e","css":[]},"style":{"size":122,"entry":"/style.6d7a32b1405ea1bb2bdf.js","hash":"6d7a32b1405ea1bb2bdf","css":["/style.6d7a32b1405ea1bb2bdf.css"]},"app":{"size":351315,"entry":"/app.d4bdff82ac1db214898b.js","hash":"d4bdff82ac1db214898b","css":[]}},"js":["/manifest.09cb8f5a05c9cfc35585.js","/vendor.f73c0104cb72cfb2809e.js","/style.6d7a32b1405ea1bb2bdf.js","/app.d4bdff82ac1db214898b.js"],"css":["/style.6d7a32b1405ea1bb2bdf.css"]},"options":{"template":"/Users/andrew/dev/clients/ES/code/event-sourcing-examples/js-frontend/node_modules/html-webpack-plugin/lib/loader.js!/Users/andrew/dev/clients/ES/code/event-sourcing-examples/js-frontend/public/index.ejs","filename":"index.html","hash":false,"inject":false,"compile":true,"favicon":false,"minify":false,"cache":true,"showErrors":true,"chunks":"all","excludeChunks":[],"title":"Money Transfer App","xhtml":false,"description":"ES Money Transfer App","appMountId":"root","googleAnalytics":{"trackingId":"UA-XXXX-XX","pageViewOnLoad":true},"mobile":true}}--></body>
|
||||
</html>
|
||||
95
js-frontend/build/manifest.09cb8f5a05c9cfc35585.js
Normal file
95
js-frontend/build/manifest.09cb8f5a05c9cfc35585.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // install a JSONP callback for chunk loading
|
||||
/******/ var parentJsonpFunction = window["webpackJsonp"];
|
||||
/******/ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
|
||||
/******/ // add "moreModules" to the modules object,
|
||||
/******/ // then flag all "chunkIds" as loaded and fire callback
|
||||
/******/ var moduleId, chunkId, i = 0, callbacks = [];
|
||||
/******/ for(;i < chunkIds.length; i++) {
|
||||
/******/ chunkId = chunkIds[i];
|
||||
/******/ if(installedChunks[chunkId])
|
||||
/******/ callbacks.push.apply(callbacks, installedChunks[chunkId]);
|
||||
/******/ installedChunks[chunkId] = 0;
|
||||
/******/ }
|
||||
/******/ for(moduleId in moreModules) {
|
||||
/******/ modules[moduleId] = moreModules[moduleId];
|
||||
/******/ }
|
||||
/******/ if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
|
||||
/******/ while(callbacks.length)
|
||||
/******/ callbacks.shift().call(null, __webpack_require__);
|
||||
/******/ if(moreModules[0]) {
|
||||
/******/ installedModules[0] = 0;
|
||||
/******/ return __webpack_require__(0);
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
/******/
|
||||
/******/ // object to store loaded and loading chunks
|
||||
/******/ // "0" means "already loaded"
|
||||
/******/ // Array means "loading", array contains callbacks
|
||||
/******/ var installedChunks = {
|
||||
/******/ 3:0
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId])
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
/******/
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ exports: {},
|
||||
/******/ id: moduleId,
|
||||
/******/ loaded: false
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.loaded = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // This file contains only the entry chunk.
|
||||
/******/ // The chunk loading function for additional chunks
|
||||
/******/ __webpack_require__.e = function requireEnsure(chunkId, callback) {
|
||||
/******/ // "0" is the signal for "already loaded"
|
||||
/******/ if(installedChunks[chunkId] === 0)
|
||||
/******/ return callback.call(null, __webpack_require__);
|
||||
/******/
|
||||
/******/ // an array means "currently loading".
|
||||
/******/ if(installedChunks[chunkId] !== undefined) {
|
||||
/******/ installedChunks[chunkId].push(callback);
|
||||
/******/ } else {
|
||||
/******/ // start chunk loading
|
||||
/******/ installedChunks[chunkId] = [callback];
|
||||
/******/ var head = document.getElementsByTagName('head')[0];
|
||||
/******/ var script = document.createElement('script');
|
||||
/******/ script.type = 'text/javascript';
|
||||
/******/ script.charset = 'utf-8';
|
||||
/******/ script.async = true;
|
||||
/******/
|
||||
/******/ script.src = __webpack_require__.p + "" + {"0":"d4bdff82ac1db214898b","1":"6d7a32b1405ea1bb2bdf","2":"f73c0104cb72cfb2809e"}[chunkId] + ".js";
|
||||
/******/ head.appendChild(script);
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
/******/
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
/******/
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "/";
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ([]);
|
||||
//# sourceMappingURL=manifest.09cb8f5a05c9cfc35585.js.map
|
||||
1
js-frontend/build/manifest.09cb8f5a05c9cfc35585.js.map
Normal file
1
js-frontend/build/manifest.09cb8f5a05c9cfc35585.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack:///webpack/bootstrap 8ac05bd1ead33e72514a?"],"names":[],"mappings":";AAAA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAQ,oBAAoB;AAC5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,YAAI;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,uDAA+C,iFAAiF;AAChI;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA","file":"manifest.09cb8f5a05c9cfc35585.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, callbacks = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId])\n \t\t\t\tcallbacks.push.apply(callbacks, installedChunks[chunkId]);\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);\n \t\twhile(callbacks.length)\n \t\t\tcallbacks.shift().call(null, __webpack_require__);\n \t\tif(moreModules[0]) {\n \t\t\tinstalledModules[0] = 0;\n \t\t\treturn __webpack_require__(0);\n \t\t}\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// \"0\" means \"already loaded\"\n \t// Array means \"loading\", array contains callbacks\n \tvar installedChunks = {\n \t\t3:0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n \t// This file contains only the entry chunk.\n \t// The chunk loading function for additional chunks\n \t__webpack_require__.e = function requireEnsure(chunkId, callback) {\n \t\t// \"0\" is the signal for \"already loaded\"\n \t\tif(installedChunks[chunkId] === 0)\n \t\t\treturn callback.call(null, __webpack_require__);\n\n \t\t// an array means \"currently loading\".\n \t\tif(installedChunks[chunkId] !== undefined) {\n \t\t\tinstalledChunks[chunkId].push(callback);\n \t\t} else {\n \t\t\t// start chunk loading\n \t\t\tinstalledChunks[chunkId] = [callback];\n \t\t\tvar head = document.getElementsByTagName('head')[0];\n \t\t\tvar script = document.createElement('script');\n \t\t\tscript.type = 'text/javascript';\n \t\t\tscript.charset = 'utf-8';\n \t\t\tscript.async = true;\n\n \t\t\tscript.src = __webpack_require__.p + \"\" + {\"0\":\"d4bdff82ac1db214898b\",\"1\":\"6d7a32b1405ea1bb2bdf\",\"2\":\"f73c0104cb72cfb2809e\"}[chunkId] + \".js\";\n \t\t\thead.appendChild(script);\n \t\t}\n \t};\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n\n\n/** WEBPACK FOOTER **\n ** webpack/bootstrap 8ac05bd1ead33e72514a\n **/"],"sourceRoot":""}
|
||||
5
js-frontend/build/robots.txt
Normal file
5
js-frontend/build/robots.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# www.robotstxt.org/
|
||||
|
||||
# Allow crawling of all content
|
||||
User-agent: *
|
||||
Disallow:
|
||||
441
js-frontend/build/style.6d7a32b1405ea1bb2bdf.css
Normal file
441
js-frontend/build/style.6d7a32b1405ea1bb2bdf.css
Normal file
@@ -0,0 +1,441 @@
|
||||
@import url(http://fonts.googleapis.com/css?family=Roboto:300,400,500);
|
||||
|
||||
/**
|
||||
* React Select
|
||||
* ============
|
||||
* Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/
|
||||
* https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs
|
||||
* MIT License: https://github.com/keystonejs/react-select
|
||||
*/
|
||||
|
||||
.Select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Select,.Select div,.Select input,.Select span {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.Select.is-disabled > .Select-control {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.Select.is-disabled .Select-arrow-zone {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.Select-control {
|
||||
background-color: #fff;
|
||||
border-color: #d9d9d9 #ccc #b3b3b3;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
color: #333;
|
||||
cursor: default;
|
||||
display: table;
|
||||
height: 36px;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Select-control:hover {
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.is-searchable.is-open > .Select-control {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.is-open > .Select-control {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
background: #fff;
|
||||
border-color: #b3b3b3 #ccc #d9d9d9;
|
||||
}
|
||||
|
||||
.is-open > .Select-control > .Select-arrow {
|
||||
border-color: transparent transparent #999;
|
||||
border-width: 0 5px 5px;
|
||||
}
|
||||
|
||||
.is-searchable.is-focused:not(.is-open) > .Select-control {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.is-focused:not(.is-open) > .Select-control {
|
||||
border-color: #08c #0099e6 #0099e6;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px rgba(0, 136, 204, 0.5);
|
||||
}
|
||||
|
||||
.Select-placeholder {
|
||||
bottom: 0;
|
||||
color: #aaa;
|
||||
left: 0;
|
||||
line-height: 34px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.has-value > .Select-control > .Select-placeholder {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.Select-value {
|
||||
color: #aaa;
|
||||
left: 0;
|
||||
padding: 8px 52px 8px 10px;
|
||||
position: absolute;
|
||||
right: -15px;
|
||||
top: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.has-value > .Select-control > .Select-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.Select-input {
|
||||
height: 34px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.Select-input > input {
|
||||
background: none transparent;
|
||||
border: 0 none;
|
||||
box-shadow: none;
|
||||
cursor: default;
|
||||
display: inline-block;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
height: 34px;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.is-focused .Select-input > input {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.Select-control:not(.is-searchable) > .Select-input {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.Select-loading-zone {
|
||||
cursor: pointer;
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.Select-loading {
|
||||
-webkit-animation: Select-animation-spin 400ms infinite linear;
|
||||
-o-animation: Select-animation-spin 400ms infinite linear;
|
||||
animation: Select-animation-spin 400ms infinite linear;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ccc;
|
||||
border-right-color: #333;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.Select-clear-zone {
|
||||
-webkit-animation: Select-animation-fadeIn 200ms;
|
||||
-o-animation: Select-animation-fadeIn 200ms;
|
||||
animation: Select-animation-fadeIn 200ms;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 17px;
|
||||
}
|
||||
|
||||
.Select-clear-zone:hover {
|
||||
color: #D0021B;
|
||||
}
|
||||
|
||||
.Select-clear {
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.Select--multi .Select-clear-zone {
|
||||
width: 17px;
|
||||
}
|
||||
|
||||
.Select-arrow-zone {
|
||||
cursor: pointer;
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 25px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.Select-arrow {
|
||||
border-color: #999 transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 2.5px;
|
||||
display: inline-block;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.is-open .Select-arrow,.Select-arrow-zone:hover > .Select-arrow {
|
||||
border-top-color: #666;
|
||||
}
|
||||
|
||||
@-webkit-keyframes Select-animation-fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes Select-animation-fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.Select-menu-outer {
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-top-color: #e6e6e6;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
box-sizing: border-box;
|
||||
margin-top: -1px;
|
||||
max-height: 200px;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.Select-menu {
|
||||
max-height: 198px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.Select-option {
|
||||
box-sizing: border-box;
|
||||
color: #666666;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.Select-option:last-child {
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.Select-option.is-focused {
|
||||
background-color: #f2f9fc;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.Select-option.is-disabled {
|
||||
color: #cccccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Select-noresults,.Select-search-prompt,.Select-searching {
|
||||
box-sizing: border-box;
|
||||
color: #999999;
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.Select--multi .Select-input {
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.Select--multi.has-value .Select-input {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.Select-item {
|
||||
background-color: #f2f9fc;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #c9e6f2;
|
||||
color: #08c;
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.Select-item-icon,.Select-item-label {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.Select-item-label {
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
cursor: default;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.Select-item-label .Select-item-label__a {
|
||||
color: #08c;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Select-item-icon {
|
||||
cursor: pointer;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-top-left-radius: 2px;
|
||||
border-right: 1px solid #c9e6f2;
|
||||
padding: 1px 5px 3px;
|
||||
}
|
||||
|
||||
.Select-item-icon:hover,.Select-item-icon:focus {
|
||||
background-color: #ddeff7;
|
||||
color: #0077b3;
|
||||
}
|
||||
|
||||
.Select-item-icon:active {
|
||||
background-color: #c9e6f2;
|
||||
}
|
||||
|
||||
.Select--multi.is-disabled .Select-item {
|
||||
background-color: #f2f2f2;
|
||||
border: 1px solid #d9d9d9;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.Select--multi.is-disabled .Select-item-icon {
|
||||
cursor: not-allowed;
|
||||
border-right: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.Select--multi.is-disabled .Select-item-icon:hover,.Select--multi.is-disabled .Select-item-icon:focus,.Select--multi.is-disabled .Select-item-icon:active {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
@keyframes Select-animation-spin {
|
||||
to {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes Select-animation-spin {
|
||||
to {
|
||||
-webkit-transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This file contains Global styles.
|
||||
*
|
||||
* In general, your styles should *not* be in this file, but in the individual
|
||||
* component files. For details, see the Pacomo specification:
|
||||
*
|
||||
* https://github.com/unicorn-standard/pacomo
|
||||
*/
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
*:before,*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,body,main {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
font-family: Roboto;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
input,button,select,textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
#react-app {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-bottom: 50px;
|
||||
/* height: 100%; */
|
||||
/* min-height: 100%; */
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.footer-navigation {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.footer-navigation > .container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.footer-navigation > .container > * {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding-bottom: 9px;
|
||||
margin: 0px 0 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: .5em;
|
||||
}
|
||||
/*# sourceMappingURL=style.6d7a32b1405ea1bb2bdf.css.map*/
|
||||
1
js-frontend/build/style.6d7a32b1405ea1bb2bdf.css.map
Normal file
1
js-frontend/build/style.6d7a32b1405ea1bb2bdf.css.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"style.6d7a32b1405ea1bb2bdf.css","sourceRoot":""}
|
||||
27
js-frontend/build/style.6d7a32b1405ea1bb2bdf.js
Normal file
27
js-frontend/build/style.6d7a32b1405ea1bb2bdf.js
Normal file
@@ -0,0 +1,27 @@
|
||||
webpackJsonp([1,3],{
|
||||
|
||||
/***/ 0:
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
__webpack_require__(614);
|
||||
module.exports = __webpack_require__(618);
|
||||
|
||||
|
||||
/***/ },
|
||||
|
||||
/***/ 614:
|
||||
/***/ function(module, exports) {
|
||||
|
||||
// removed by extract-text-webpack-plugin
|
||||
|
||||
/***/ },
|
||||
|
||||
/***/ 618:
|
||||
/***/ function(module, exports) {
|
||||
|
||||
// removed by extract-text-webpack-plugin
|
||||
|
||||
/***/ }
|
||||
|
||||
});
|
||||
//# sourceMappingURL=style.6d7a32b1405ea1bb2bdf.js.map
|
||||
1
js-frontend/build/style.6d7a32b1405ea1bb2bdf.js.map
Normal file
1
js-frontend/build/style.6d7a32b1405ea1bb2bdf.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack:///./~/react-select/dist/react-select.css?","webpack:///./src/main.less?"],"names":[],"mappings":";;;;;;;;;;;;;;AAAA,0C;;;;;;;ACAA,0C","file":"style.6d7a32b1405ea1bb2bdf.js","sourcesContent":["// removed by extract-text-webpack-plugin\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./~/react-select/dist/react-select.css\n ** module id = 614\n ** module chunks = 1\n **/","// removed by extract-text-webpack-plugin\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./src/main.less\n ** module id = 618\n ** module chunks = 1\n **/"],"sourceRoot":""}
|
||||
334
js-frontend/build/style2.css
Normal file
334
js-frontend/build/style2.css
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* React Select
|
||||
* ============
|
||||
* Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/
|
||||
* https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs
|
||||
* MIT License: https://github.com/keystonejs/react-select
|
||||
*/
|
||||
.Select {
|
||||
position: relative;
|
||||
}
|
||||
.Select,
|
||||
.Select div,
|
||||
.Select input,
|
||||
.Select span {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.Select.is-disabled > .Select-control {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.Select.is-disabled > .Select-control:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
.Select.is-disabled .Select-arrow-zone {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
.Select-control {
|
||||
background-color: #fff;
|
||||
border-color: #d9d9d9 #ccc #b3b3b3;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
color: #333;
|
||||
cursor: default;
|
||||
display: table;
|
||||
height: 36px;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.Select-control:hover {
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.is-searchable.is-open > .Select-control {
|
||||
cursor: text;
|
||||
}
|
||||
.is-open > .Select-control {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
background: #fff;
|
||||
border-color: #b3b3b3 #ccc #d9d9d9;
|
||||
}
|
||||
.is-open > .Select-control > .Select-arrow {
|
||||
border-color: transparent transparent #999;
|
||||
border-width: 0 5px 5px;
|
||||
}
|
||||
.is-searchable.is-focused:not(.is-open) > .Select-control {
|
||||
cursor: text;
|
||||
}
|
||||
.is-focused:not(.is-open) > .Select-control {
|
||||
border-color: #007eff;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 3px rgba(0, 126, 255, 0.1);
|
||||
}
|
||||
.Select-placeholder,
|
||||
:not(.Select--multi) > .Select-control .Select-value {
|
||||
bottom: 0;
|
||||
color: #aaa;
|
||||
left: 0;
|
||||
line-height: 34px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.has-value:not(.Select--multi) > .Select-control > .Select-value .Select-value-label,
|
||||
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value .Select-value-label {
|
||||
color: #333;
|
||||
}
|
||||
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label,
|
||||
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover,
|
||||
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover,
|
||||
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus,
|
||||
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus {
|
||||
color: #007eff;
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.Select-input {
|
||||
height: 34px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.Select-input > input {
|
||||
background: none transparent;
|
||||
border: 0 none;
|
||||
box-shadow: none;
|
||||
cursor: default;
|
||||
display: inline-block;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
height: 34px;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.is-focused .Select-input > input {
|
||||
cursor: text;
|
||||
}
|
||||
.has-value.is-pseudo-focused .Select-input {
|
||||
opacity: 0;
|
||||
}
|
||||
.Select-control:not(.is-searchable) > .Select-input {
|
||||
outline: none;
|
||||
}
|
||||
.Select-loading-zone {
|
||||
cursor: pointer;
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 16px;
|
||||
}
|
||||
.Select-loading {
|
||||
-webkit-animation: Select-animation-spin 400ms infinite linear;
|
||||
-o-animation: Select-animation-spin 400ms infinite linear;
|
||||
animation: Select-animation-spin 400ms infinite linear;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ccc;
|
||||
border-right-color: #333;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.Select-clear-zone {
|
||||
-webkit-animation: Select-animation-fadeIn 200ms;
|
||||
-o-animation: Select-animation-fadeIn 200ms;
|
||||
animation: Select-animation-fadeIn 200ms;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 17px;
|
||||
}
|
||||
.Select-clear-zone:hover {
|
||||
color: #D0021B;
|
||||
}
|
||||
.Select-clear {
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
.Select--multi .Select-clear-zone {
|
||||
width: 17px;
|
||||
}
|
||||
.Select-arrow-zone {
|
||||
cursor: pointer;
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 25px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.Select-arrow {
|
||||
border-color: #999 transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 2.5px;
|
||||
display: inline-block;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
.is-open .Select-arrow,
|
||||
.Select-arrow-zone:hover > .Select-arrow {
|
||||
border-top-color: #666;
|
||||
}
|
||||
@-webkit-keyframes Select-animation-fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes Select-animation-fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.Select-menu-outer {
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-top-color: #e6e6e6;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
box-sizing: border-box;
|
||||
margin-top: -1px;
|
||||
max-height: 200px;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.Select-menu {
|
||||
max-height: 198px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.Select-option {
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
color: #666666;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.Select-option:last-child {
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.Select-option.is-focused {
|
||||
background-color: rgba(0, 126, 255, 0.08);
|
||||
color: #333;
|
||||
}
|
||||
.Select-option.is-disabled {
|
||||
color: #cccccc;
|
||||
cursor: default;
|
||||
}
|
||||
.Select-noresults {
|
||||
box-sizing: border-box;
|
||||
color: #999999;
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.Select--multi .Select-input {
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
.Select--multi.has-value .Select-input {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.Select--multi .Select-value {
|
||||
background-color: rgba(0, 126, 255, 0.08);
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(0, 126, 255, 0.24);
|
||||
color: #007eff;
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.Select--multi .Select-value-icon,
|
||||
.Select--multi .Select-value-label {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.Select--multi .Select-value-label {
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
cursor: default;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.Select--multi a.Select-value-label {
|
||||
color: #007eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.Select--multi a.Select-value-label:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.Select--multi .Select-value-icon {
|
||||
cursor: pointer;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-top-left-radius: 2px;
|
||||
border-right: 1px solid rgba(0, 126, 255, 0.24);
|
||||
padding: 1px 5px 3px;
|
||||
}
|
||||
.Select--multi .Select-value-icon:hover,
|
||||
.Select--multi .Select-value-icon:focus {
|
||||
background-color: rgba(0, 113, 230, 0.08);
|
||||
color: #0071e6;
|
||||
}
|
||||
.Select--multi .Select-value-icon:active {
|
||||
background-color: rgba(0, 126, 255, 0.24);
|
||||
}
|
||||
.Select--multi.is-disabled .Select-value {
|
||||
background-color: #fcfcfc;
|
||||
border: 1px solid #e3e3e3;
|
||||
color: #333;
|
||||
}
|
||||
.Select--multi.is-disabled .Select-value-icon {
|
||||
cursor: not-allowed;
|
||||
border-right: 1px solid #e3e3e3;
|
||||
}
|
||||
.Select--multi.is-disabled .Select-value-icon:hover,
|
||||
.Select--multi.is-disabled .Select-value-icon:focus,
|
||||
.Select--multi.is-disabled .Select-value-icon:active {
|
||||
background-color: #fcfcfc;
|
||||
}
|
||||
@keyframes Select-animation-spin {
|
||||
to {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes Select-animation-spin {
|
||||
to {
|
||||
-webkit-transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
55905
js-frontend/build/vendor.f73c0104cb72cfb2809e.js
Normal file
55905
js-frontend/build/vendor.f73c0104cb72cfb2809e.js
Normal file
File diff suppressed because it is too large
Load Diff
1
js-frontend/build/vendor.f73c0104cb72cfb2809e.js.map
Normal file
1
js-frontend/build/vendor.f73c0104cb72cfb2809e.js.map
Normal file
File diff suppressed because one or more lines are too long
217
js-frontend/config/webpackConfigParts.js
Normal file
217
js-frontend/config/webpackConfigParts.js
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Created by andrew on 8/18/16.
|
||||
*/
|
||||
const webpack = require('webpack');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const PurifyCSSPlugin = require('purifycss-webpack-plugin');
|
||||
|
||||
exports.devServer = function(options) {
|
||||
return {
|
||||
entry: {
|
||||
'webpack-dev-server': 'webpack-dev-server/client?http://localhost:8080',
|
||||
hmr: 'webpack/hot/only-dev-server'
|
||||
},
|
||||
devServer: {
|
||||
contentBase: './build',
|
||||
hot: true,
|
||||
historyApiFallback: true,
|
||||
inline: true,
|
||||
stats: 'errors-only',
|
||||
host: options.host, // Defaults to `localhost`
|
||||
port: options.port, // Defaults to 8080
|
||||
proxy: {
|
||||
'/api*' : {
|
||||
target: 'http://localhost:8080'
|
||||
}
|
||||
// '/user*' : {
|
||||
// target: 'http://localhost:8080'
|
||||
// },
|
||||
// '/login' : {
|
||||
// target: 'http://localhost:8080'
|
||||
// },
|
||||
// '/customers*' : {
|
||||
// target: 'http://localhost:8080'
|
||||
// },
|
||||
// '/accounts*' : {
|
||||
// target: 'http://localhost:8080'
|
||||
// },
|
||||
// '/transfers*' : {
|
||||
// target: 'http://localhost:8080'
|
||||
// }
|
||||
}
|
||||
},
|
||||
watchOptions: {
|
||||
// Delay the rebuild after the first change
|
||||
aggregateTimeout: 300,
|
||||
// Poll using interval (in ms, accepts boolean too)
|
||||
poll: 1000
|
||||
},
|
||||
plugins: [
|
||||
// Enable multi-pass compilation for enhanced performance
|
||||
// in larger projects. Good default.
|
||||
new webpack.HotModuleReplacementPlugin({
|
||||
multiStep: true
|
||||
})
|
||||
]
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
exports.setupCSS = function(paths) {
|
||||
return {
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
loaders: ['style', 'css'],
|
||||
include: paths
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
exports.setupLess = function(paths) {
|
||||
return {
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.(le)|(c)ss$/,
|
||||
loaders: ['style', 'css', 'less'],
|
||||
include: paths
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
exports.useJSON = function() {
|
||||
return {
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.json$/,
|
||||
loaders: ['json']
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
exports.minify = function() {
|
||||
return {
|
||||
plugins: [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
}
|
||||
})
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
exports.extractBundle = function(options) {
|
||||
const entry = {};
|
||||
entry[options.name] = options.entries;
|
||||
|
||||
return {
|
||||
// Define an entry point needed for splitting.
|
||||
entry: entry,
|
||||
plugins: [
|
||||
// Extract bundle and manifest files. Manifest is
|
||||
// needed for reliable caching.
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
names: [options.name, 'manifest']
|
||||
})
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
exports.clean = function(path) {
|
||||
return {
|
||||
plugins: [
|
||||
new CleanWebpackPlugin([path], {
|
||||
// Without `root` CleanWebpackPlugin won't point to our
|
||||
// project and will fail to work.
|
||||
root: process.cwd()
|
||||
})
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
exports.extractCSS = function(paths) {
|
||||
return {
|
||||
module: {
|
||||
loaders: [
|
||||
// Extract CSS during build
|
||||
{
|
||||
test: /\.css$/,
|
||||
// loaders: ['style', 'css'],
|
||||
loader: ExtractTextPlugin.extract('style', 'css'),
|
||||
include: paths
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
// Output extracted CSS to a file
|
||||
new ExtractTextPlugin('[name].[chunkhash].css')
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
exports.extractLESS = function(paths) {
|
||||
return {
|
||||
module: {
|
||||
loaders: [
|
||||
// Extract CSS during build
|
||||
{
|
||||
test: /\.(le)|(c)ss$/,
|
||||
// loaders: ['style', 'css', 'less'],
|
||||
loader: ExtractTextPlugin.extract(
|
||||
"style-loader",
|
||||
'css?sourceMap!' +
|
||||
'less?sourceMap'
|
||||
),
|
||||
include: paths
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
// Output extracted CSS to a file
|
||||
new ExtractTextPlugin('[name].[chunkhash].css')
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
exports.purifyCSS = function(paths) {
|
||||
return {
|
||||
plugins: [
|
||||
new PurifyCSSPlugin({
|
||||
basePath: process.cwd(),
|
||||
// `paths` is used to point PurifyCSS to files not
|
||||
// visible to Webpack. You can pass glob patterns
|
||||
// to it.
|
||||
paths: paths,
|
||||
purifyOptions: {
|
||||
// minify: false,
|
||||
// info: true,
|
||||
// output: './output.css'
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
exports.useJQuery = function() {
|
||||
return {
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
$: "jquery",
|
||||
jQuery: "jquery"
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"name": "event-sourcing-examples",
|
||||
"description": "ES Money Transfer App",
|
||||
"author": "cer",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
@@ -9,20 +9,26 @@
|
||||
"scripts": {
|
||||
"start": "gulp serve",
|
||||
"open": "gulp open",
|
||||
"gulp": "gulp"
|
||||
"gulp": "gulp",
|
||||
"build": "webpack",
|
||||
"watch": "webpack --progress --colors --watch --display-error-details --display-chunks --profile",
|
||||
"start-dev": "export PORT=3000 && webpack-dev-server --host 0.0.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer-loader": "^2.0.0",
|
||||
"babel-core": "6.1.4",
|
||||
"babel-loader": "6.1.0",
|
||||
"babel-plugin-add-module-exports": "^0.1.2",
|
||||
"babel-plugin-transform-runtime": "6.1.4",
|
||||
"babel-polyfill": "^6.1.4",
|
||||
"babel-preset-es2015": "6.1.4",
|
||||
"babel-preset-react": "6.1.4",
|
||||
"babel-preset-stage-0": "6.1.2",
|
||||
"babel-register": "6.1.4",
|
||||
"babel-runtime": "^6.0.14",
|
||||
"babel-plugin-transform-runtime": "^6.12.0",
|
||||
|
||||
"babel-cli": "^6.7.7",
|
||||
"babel-core": "^6.10.4",
|
||||
"babel-eslint": "^4.1.6",
|
||||
"babel-loader": "^6.2.4",
|
||||
"babel-polyfill": "^6.2.0",
|
||||
"babel-preset-es2015": "^6.9.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.1.2",
|
||||
"clean-webpack-plugin": "^0.1.10",
|
||||
"copy-webpack-plugin": "^3.0.1",
|
||||
"css-loader": "^0.14.4",
|
||||
"del": "^1.2.0",
|
||||
"extract-text-webpack-plugin": "^0.8.1",
|
||||
@@ -35,17 +41,22 @@
|
||||
"gulp-load-plugins": "^0.10.0",
|
||||
"gulp-size": "^1.2.1",
|
||||
"gulp-util": "^3.0.5",
|
||||
"html-webpack-plugin": "^2.22.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"less": "^2.5.3",
|
||||
"less-loader": "^2.2.0",
|
||||
"node-libs-browser": "^0.5.2",
|
||||
"open": "0.0.5",
|
||||
"purifycss-webpack-plugin": "^2.0.3",
|
||||
"react-hot-loader": "^1.3.0",
|
||||
"redux-devtools": "^2.1.5",
|
||||
"run-sequence": "^1.1.0",
|
||||
"style-loader": "^0.12.3",
|
||||
"url-loader": "^0.5.6",
|
||||
"webpack": "^1.9.10",
|
||||
"webpack-dev-server": "^1.9.0"
|
||||
"webpack-dev-server": "^1.9.0",
|
||||
"webpack-merge": "^0.14.1",
|
||||
"webpack-validator": "^2.2.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.3",
|
||||
@@ -59,10 +70,10 @@
|
||||
"react": "^0.14.7",
|
||||
"react-bootstrap": "^0.28.3",
|
||||
"react-dom": "^0.14.0",
|
||||
"react-loader": "^2.0.0",
|
||||
"react-loader": "^2.4.0",
|
||||
"react-pacomo": "^0.5.1",
|
||||
"react-redux": "^4.4.0",
|
||||
"react-router": "^2.0.0-rc2",
|
||||
"react-router": "^2.7.0",
|
||||
"react-router-bootstrap": "^0.20.1",
|
||||
"react-router-redux": "^3.0.0",
|
||||
"react-select": "^0.9.1",
|
||||
|
||||
0
js-frontend/public/.gitkeep
Normal file
0
js-frontend/public/.gitkeep
Normal file
81
js-frontend/public/index.ejs
Normal file
81
js-frontend/public/index.ejs
Normal file
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<!--[if lt IE 7 ]> <html lang="en" class="ie6" <% if(htmlWebpackPlugin.files.manifest) { %> manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> <![endif]-->
|
||||
<!--[if IE 7 ]> <html lang="en" class="ie7" <% if(htmlWebpackPlugin.files.manifest) { %> manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> <![endif]-->
|
||||
<!--[if IE 8 ]> <html lang="en" class="ie8" <% if(htmlWebpackPlugin.files.manifest) { %> manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> <![endif]-->
|
||||
<!--[if IE 9 ]> <html lang="en" class="ie9" <% if(htmlWebpackPlugin.files.manifest) { %> manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> <![endif]-->
|
||||
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="" <% if(htmlWebpackPlugin.files.manifest) { %> manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title><%= htmlWebpackPlugin.options.title || 'Webpack App'%></title><%
|
||||
if (htmlWebpackPlugin.options.description) {
|
||||
%><meta name="description" content="<%= htmlWebpackPlugin.options.description%>" /><%
|
||||
} %><%
|
||||
if (htmlWebpackPlugin.files.favicon) {
|
||||
%><link rel="shortcut icon" href="<%= htmlWebpackPlugin.files.favicon%>"><%
|
||||
} %><%
|
||||
if (htmlWebpackPlugin.options.mobile) {
|
||||
%><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" /><%
|
||||
} %><!-- Latest compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">
|
||||
|
||||
<!-- Optional theme -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap-theme.min.css"><%
|
||||
for (var css in htmlWebpackPlugin.files.css) {
|
||||
%><link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet"><%
|
||||
} %><%
|
||||
if (htmlWebpackPlugin.options.baseHref) {
|
||||
%><base href="<%= htmlWebpackPlugin.options.baseHref %>" /><%
|
||||
} %></head>
|
||||
<body><%
|
||||
if (htmlWebpackPlugin.options.unsupportedBrowser) {
|
||||
%><style>.unsupported-browser { display: none; }</style>
|
||||
<div class="unsupported-browser">
|
||||
Sorry, your browser is not supported. Please upgrade to
|
||||
the latest version or switch your browser to use this site.
|
||||
See <a href="http://outdatedbrowser.com/">outdatedbrowser.com</a>
|
||||
for options.
|
||||
</div><%
|
||||
} %><%
|
||||
if (htmlWebpackPlugin.options.appMountId) {
|
||||
%><div id="<%= htmlWebpackPlugin.options.appMountId%>"></div><%
|
||||
} %><%
|
||||
if (htmlWebpackPlugin.options.appMountIds && htmlWebpackPlugin.options.appMountIds.length > 0) {
|
||||
%><%
|
||||
for (var index in htmlWebpackPlugin.options.appMountIds) {
|
||||
%><div id="<%= htmlWebpackPlugin.options.appMountIds[index]%>"></div><%
|
||||
} %><%
|
||||
} %><%
|
||||
if (htmlWebpackPlugin.options.window) {
|
||||
%><script>
|
||||
<%
|
||||
for (var varName in htmlWebpackPlugin.options.window) { %>window['<%=varName%>'] = <%= JSON.stringify(htmlWebpackPlugin.options.window[varName]) %>;<%
|
||||
} %>
|
||||
</script><%
|
||||
} %><%
|
||||
for (var chunk in htmlWebpackPlugin.files.chunks) {
|
||||
%><script src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script><%
|
||||
} %><%
|
||||
if (htmlWebpackPlugin.options.devServer) {
|
||||
%><script src="<%= htmlWebpackPlugin.options.devServer%>/webpack-dev-server.js"></script><%
|
||||
} %><%
|
||||
if (htmlWebpackPlugin.options.googleAnalytics) {
|
||||
%><script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
|
||||
<% if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %>
|
||||
ga('create', '<%= htmlWebpackPlugin.options.googleAnalytics.trackingId%>', 'auto');
|
||||
<% } else { throw new Error("html-webpack-template requires googleAnalytics.trackingId config"); }%>
|
||||
|
||||
<% if (htmlWebpackPlugin.options.googleAnalytics.pageViewOnLoad) { %>
|
||||
ga('send', 'pageview');
|
||||
<% } %>
|
||||
</script><%
|
||||
}
|
||||
%><!--<%= JSON.stringify(htmlWebpackPlugin)%>--></body>
|
||||
</html>
|
||||
5
js-frontend/public/robots.txt
Executable file
5
js-frontend/public/robots.txt
Executable file
@@ -0,0 +1,5 @@
|
||||
# www.robotstxt.org/
|
||||
|
||||
# Allow crawling of all content
|
||||
User-agent: *
|
||||
Disallow:
|
||||
334
js-frontend/public/style2.css
Normal file
334
js-frontend/public/style2.css
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* React Select
|
||||
* ============
|
||||
* Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/
|
||||
* https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs
|
||||
* MIT License: https://github.com/keystonejs/react-select
|
||||
*/
|
||||
.Select {
|
||||
position: relative;
|
||||
}
|
||||
.Select,
|
||||
.Select div,
|
||||
.Select input,
|
||||
.Select span {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.Select.is-disabled > .Select-control {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.Select.is-disabled > .Select-control:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
.Select.is-disabled .Select-arrow-zone {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
.Select-control {
|
||||
background-color: #fff;
|
||||
border-color: #d9d9d9 #ccc #b3b3b3;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
color: #333;
|
||||
cursor: default;
|
||||
display: table;
|
||||
height: 36px;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.Select-control:hover {
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.is-searchable.is-open > .Select-control {
|
||||
cursor: text;
|
||||
}
|
||||
.is-open > .Select-control {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
background: #fff;
|
||||
border-color: #b3b3b3 #ccc #d9d9d9;
|
||||
}
|
||||
.is-open > .Select-control > .Select-arrow {
|
||||
border-color: transparent transparent #999;
|
||||
border-width: 0 5px 5px;
|
||||
}
|
||||
.is-searchable.is-focused:not(.is-open) > .Select-control {
|
||||
cursor: text;
|
||||
}
|
||||
.is-focused:not(.is-open) > .Select-control {
|
||||
border-color: #007eff;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 3px rgba(0, 126, 255, 0.1);
|
||||
}
|
||||
.Select-placeholder,
|
||||
:not(.Select--multi) > .Select-control .Select-value {
|
||||
bottom: 0;
|
||||
color: #aaa;
|
||||
left: 0;
|
||||
line-height: 34px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.has-value:not(.Select--multi) > .Select-control > .Select-value .Select-value-label,
|
||||
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value .Select-value-label {
|
||||
color: #333;
|
||||
}
|
||||
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label,
|
||||
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover,
|
||||
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover,
|
||||
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus,
|
||||
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus {
|
||||
color: #007eff;
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.Select-input {
|
||||
height: 34px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.Select-input > input {
|
||||
background: none transparent;
|
||||
border: 0 none;
|
||||
box-shadow: none;
|
||||
cursor: default;
|
||||
display: inline-block;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
height: 34px;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.is-focused .Select-input > input {
|
||||
cursor: text;
|
||||
}
|
||||
.has-value.is-pseudo-focused .Select-input {
|
||||
opacity: 0;
|
||||
}
|
||||
.Select-control:not(.is-searchable) > .Select-input {
|
||||
outline: none;
|
||||
}
|
||||
.Select-loading-zone {
|
||||
cursor: pointer;
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 16px;
|
||||
}
|
||||
.Select-loading {
|
||||
-webkit-animation: Select-animation-spin 400ms infinite linear;
|
||||
-o-animation: Select-animation-spin 400ms infinite linear;
|
||||
animation: Select-animation-spin 400ms infinite linear;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ccc;
|
||||
border-right-color: #333;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.Select-clear-zone {
|
||||
-webkit-animation: Select-animation-fadeIn 200ms;
|
||||
-o-animation: Select-animation-fadeIn 200ms;
|
||||
animation: Select-animation-fadeIn 200ms;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 17px;
|
||||
}
|
||||
.Select-clear-zone:hover {
|
||||
color: #D0021B;
|
||||
}
|
||||
.Select-clear {
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
.Select--multi .Select-clear-zone {
|
||||
width: 17px;
|
||||
}
|
||||
.Select-arrow-zone {
|
||||
cursor: pointer;
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 25px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.Select-arrow {
|
||||
border-color: #999 transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 2.5px;
|
||||
display: inline-block;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
.is-open .Select-arrow,
|
||||
.Select-arrow-zone:hover > .Select-arrow {
|
||||
border-top-color: #666;
|
||||
}
|
||||
@-webkit-keyframes Select-animation-fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes Select-animation-fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.Select-menu-outer {
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-top-color: #e6e6e6;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
box-sizing: border-box;
|
||||
margin-top: -1px;
|
||||
max-height: 200px;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.Select-menu {
|
||||
max-height: 198px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.Select-option {
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
color: #666666;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.Select-option:last-child {
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.Select-option.is-focused {
|
||||
background-color: rgba(0, 126, 255, 0.08);
|
||||
color: #333;
|
||||
}
|
||||
.Select-option.is-disabled {
|
||||
color: #cccccc;
|
||||
cursor: default;
|
||||
}
|
||||
.Select-noresults {
|
||||
box-sizing: border-box;
|
||||
color: #999999;
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.Select--multi .Select-input {
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
.Select--multi.has-value .Select-input {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.Select--multi .Select-value {
|
||||
background-color: rgba(0, 126, 255, 0.08);
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(0, 126, 255, 0.24);
|
||||
color: #007eff;
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.Select--multi .Select-value-icon,
|
||||
.Select--multi .Select-value-label {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.Select--multi .Select-value-label {
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
cursor: default;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.Select--multi a.Select-value-label {
|
||||
color: #007eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.Select--multi a.Select-value-label:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.Select--multi .Select-value-icon {
|
||||
cursor: pointer;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-top-left-radius: 2px;
|
||||
border-right: 1px solid rgba(0, 126, 255, 0.24);
|
||||
padding: 1px 5px 3px;
|
||||
}
|
||||
.Select--multi .Select-value-icon:hover,
|
||||
.Select--multi .Select-value-icon:focus {
|
||||
background-color: rgba(0, 113, 230, 0.08);
|
||||
color: #0071e6;
|
||||
}
|
||||
.Select--multi .Select-value-icon:active {
|
||||
background-color: rgba(0, 126, 255, 0.24);
|
||||
}
|
||||
.Select--multi.is-disabled .Select-value {
|
||||
background-color: #fcfcfc;
|
||||
border: 1px solid #e3e3e3;
|
||||
color: #333;
|
||||
}
|
||||
.Select--multi.is-disabled .Select-value-icon {
|
||||
cursor: not-allowed;
|
||||
border-right: 1px solid #e3e3e3;
|
||||
}
|
||||
.Select--multi.is-disabled .Select-value-icon:hover,
|
||||
.Select--multi.is-disabled .Select-value-icon:focus,
|
||||
.Select--multi.is-disabled .Select-value-icon:active {
|
||||
background-color: #fcfcfc;
|
||||
}
|
||||
@keyframes Select-animation-spin {
|
||||
to {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes Select-animation-spin {
|
||||
to {
|
||||
-webkit-transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
@@ -5,47 +5,29 @@
|
||||
import React from "react";
|
||||
import { createStore, compose, applyMiddleware, combineReducers} from "redux";
|
||||
import { Provider, connect} from "react-redux";
|
||||
|
||||
import thunk from "redux-thunk";
|
||||
import createLogger from 'redux-logger';
|
||||
|
||||
import { Route, IndexRoute, Link, IndexLink } from "react-router";
|
||||
import { ReduxRouter} from "redux-router";
|
||||
|
||||
//import { Router, IndexRoute, Route, browserHistory } from 'react-router';
|
||||
//import { syncHistory, routeReducer } from 'react-router-redux';
|
||||
|
||||
//import { configure as reduxAuthConfigure, authStateReducer } from "redux-auth";
|
||||
//import { authStateReducer } from "redux-auth";
|
||||
//import authStateReducer from './reducers/auth';
|
||||
//import appStateReducer from './reducers/data';
|
||||
import mainReducer from './reducers';
|
||||
|
||||
import { configure as reduxAuthConfigure } from './actions/configure';
|
||||
//import { AuthGlobals } from "redux-auth/bootstrap-theme";
|
||||
|
||||
import { createHistory, createHashHistory, createMemoryHistory } from "history";
|
||||
import { pushState, routerStateReducer, reduxReactRouter as clientRouter} from "redux-router";
|
||||
import { reduxReactRouter as serverRouter } from "redux-router/server";
|
||||
|
||||
import { requireAuthentication } from './components/AuthComponent';
|
||||
import mainReducer from './reducers';
|
||||
|
||||
//import demoButtons from "./reducers/request-test-buttons";
|
||||
//import demoUi from "./reducers/demo-ui";
|
||||
import { configure as endpointsConfig } from './actions/configure';
|
||||
import { requireAuthentication } from './components/AuthComponent';
|
||||
import Container from "./components/partials/Container";
|
||||
import MyAccounts from "./views/MyAccounts";
|
||||
import Account from "./views/Account";
|
||||
import SignIn from "./views/SignIn";
|
||||
import SignUp from "./views/SignUp";
|
||||
//import GlobalComponents from "./views/partials/GlobalComponents";
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Container>
|
||||
return (<Container>
|
||||
{this.props.children}
|
||||
</Container>
|
||||
);
|
||||
</Container>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,15 +64,15 @@ export function initialize({cookies, isServer, currentLocation, userAgent} = {})
|
||||
/**
|
||||
* The React Router 1.0 routes for both the server and the client.
|
||||
*/
|
||||
return store.dispatch(reduxAuthConfigure([
|
||||
return store.dispatch(endpointsConfig([
|
||||
{
|
||||
default: {
|
||||
//apiUrl: '/',
|
||||
emailSignInPath: '/login',
|
||||
customersPath: '/customers',
|
||||
currentUserPath: '/user',
|
||||
accountsPath: '/accounts',
|
||||
transfersPath: '/transfers'
|
||||
emailSignInPath: '/api/login',
|
||||
customersPath: '/api/customers',
|
||||
currentUserPath: '/api/user',
|
||||
accountsPath: '/api/accounts',
|
||||
transfersPath: '/api/transfers'
|
||||
}
|
||||
}
|
||||
], {
|
||||
|
||||
@@ -2,65 +2,41 @@
|
||||
* Created by andrew on 26/02/16.
|
||||
*/
|
||||
import T from '../constants/ACTION_TYPES';
|
||||
import { makeActionCreator } from '../utils/actions';
|
||||
import * as U from '../utils/sessionStorage';
|
||||
|
||||
import {
|
||||
getCurrentSettings,
|
||||
setCurrentSettings,
|
||||
getInitialEndpointKey,
|
||||
setDefaultEndpointKey,
|
||||
setCurrentEndpoint,
|
||||
setCurrentEndpointKey,
|
||||
retrieveData,
|
||||
persistData,
|
||||
destroySession,
|
||||
persistUserData,
|
||||
retrieveUserData,
|
||||
retrieveHeaders
|
||||
} from "../utils/sessionStorage";
|
||||
|
||||
import {
|
||||
apiGetCurrentUser
|
||||
} from '../utils/api';
|
||||
|
||||
import {entityReceived } from './entities';
|
||||
|
||||
export function authenticateStart() {
|
||||
return { type: T.AUTH.AUTHENTICATE_START };
|
||||
}
|
||||
export function authenticateComplete(user) {
|
||||
return { type: T.AUTH.AUTHENTICATE_COMPLETE, user };
|
||||
}
|
||||
export function authenticateError(errors) {
|
||||
return { type: T.AUTH.AUTHENTICATE_ERROR, errors };
|
||||
}
|
||||
import { apiGetCurrentUser } from '../utils/api';
|
||||
import { entityReceived } from './entities';
|
||||
|
||||
export const authenticateStart = makeActionCreator(T.AUTH.AUTHENTICATE_START);
|
||||
export const authenticateComplete = makeActionCreator(T.AUTH.AUTHENTICATE_COMPLETE, 'user');
|
||||
export const authenticateError = makeActionCreator(T.AUTH.AUTHENTICATE_ERROR, 'errors');
|
||||
|
||||
export function authenticate(forceReread) {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
|
||||
dispatch(authenticateStart());
|
||||
|
||||
const savedUserPromise = new Promise((rs, rj) => {
|
||||
|
||||
const currentHeaders = retrieveHeaders();
|
||||
const currentHeaders = U.retrieveHeaders();
|
||||
const accessToken = currentHeaders["access-token"];
|
||||
|
||||
if (!accessToken) {
|
||||
return rj({ reason: 'no token'});
|
||||
}
|
||||
|
||||
const savedUser = retrieveUserData();
|
||||
const savedUser = U.retrieveUserData();
|
||||
|
||||
if (savedUser && !forceReread) {
|
||||
return rs(savedUser);
|
||||
}
|
||||
|
||||
return apiGetCurrentUser().then((userData) => {
|
||||
persistUserData(userData);
|
||||
U.persistUserData(userData);
|
||||
dispatch(entityReceived(userData.id, userData));
|
||||
rs(userData);
|
||||
}, (err) => {
|
||||
debugger;
|
||||
rj(err);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import T from '../constants/ACTION_TYPES';
|
||||
import { makeActionCreator } from '../utils/actions';
|
||||
import * as api from '../utils/api';
|
||||
import { authenticate } from './authenticate';
|
||||
import root from '../utils/root';
|
||||
|
||||
export const entityRequested = makeActionCreator(T.ENTITIES.REQUESTED, 'id');
|
||||
export const entityReceived = makeActionCreator(T.ENTITIES.RECEIVED, 'id', 'entity');
|
||||
@@ -44,18 +45,47 @@ export function accountsList(userId) {
|
||||
};
|
||||
}
|
||||
|
||||
function readUntilChanged(initialData, customerId) {
|
||||
const initialDataFlat = root['JSON'].stringify(initialData);
|
||||
debugger;
|
||||
return new Promise((rs, rj) => {
|
||||
setTimeout(() => {
|
||||
api.apiRetrieveAccounts(customerId)
|
||||
.then(data => {
|
||||
debugger;
|
||||
if (initialDataFlat == root['JSON'].stringify(data)) {
|
||||
return readUntilChanged.call(this, data, customerId).then(rs, rj); // Promise
|
||||
}
|
||||
rs(data);
|
||||
})
|
||||
.catch(rj)
|
||||
}, 500);
|
||||
})
|
||||
}
|
||||
|
||||
export function accountCreate(customerId, payload) {
|
||||
return dispatch => {
|
||||
dispatch(accountCreateStart());
|
||||
return api.apiCreateAccount(customerId, payload)
|
||||
.then(({ accountId }) => {
|
||||
dispatch(accountCreateComplete({
|
||||
id: accountId,
|
||||
...payload
|
||||
}));
|
||||
// dispatch(entityReceived(accountId, payload));
|
||||
dispatch(authenticate(true));
|
||||
return accountId;
|
||||
.then(data => {
|
||||
if (data.accountId) {
|
||||
const { accountId } = data;
|
||||
dispatch(accountCreateComplete({
|
||||
id: accountId,
|
||||
...payload
|
||||
}));
|
||||
// dispatch(entityReceived(accountId, payload));
|
||||
dispatch(authenticate(true));
|
||||
return accountId;
|
||||
} else {
|
||||
return readUntilChanged(data, customerId)
|
||||
.then(() => {
|
||||
dispatch(accountCreateComplete({
|
||||
id: ''
|
||||
}));
|
||||
dispatch(authenticate(true));
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
debugger;
|
||||
@@ -93,7 +123,7 @@ export function fetchOwnAccounts(customerId) {
|
||||
//dispatch(accountsListRequested());
|
||||
return api.apiRetrieveAccounts(customerId)
|
||||
.then(data => {
|
||||
dispatch(accountsListReceived(data));
|
||||
dispatch(accountsListReceived(data.accounts));
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -217,20 +247,20 @@ export const makeTransfer = (accountId, payload) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getTransfersRequested = makeActionCreator(T.TRANSFERS.LIST_START);
|
||||
export const getTransfersComplete = makeActionCreator(T.TRANSFERS.LIST_COMPLETE, 'payload');
|
||||
export const getTransfersError = makeActionCreator(T.TRANSFERS.LIST_ERROR, 'error');
|
||||
export const getTransfersRequested = makeActionCreator(T.TRANSFERS.LIST_START, 'id');
|
||||
export const getTransfersComplete = makeActionCreator(T.TRANSFERS.LIST_COMPLETE, 'id', 'payload');
|
||||
export const getTransfersError = makeActionCreator(T.TRANSFERS.LIST_ERROR, 'id', 'error');
|
||||
|
||||
export const getTransfers = (accountId) => {
|
||||
return dispatch => {
|
||||
dispatch(getTransfersRequested());
|
||||
dispatch(getTransfersRequested(accountId));
|
||||
return api.apiRetrieveTransfers(accountId)
|
||||
.then(data => {
|
||||
dispatch(getTransfersComplete(data));
|
||||
dispatch(getTransfersComplete(accountId, data.transactionsHistory));
|
||||
return data;
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(getTransfersError(err));
|
||||
dispatch(getTransfersError(accountId, err));
|
||||
return err;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { initialize } from "./app";
|
||||
|
||||
|
||||
/**
|
||||
* Fire-up React Router.
|
||||
*/
|
||||
const reactRoot = window.document.getElementById("react-app");
|
||||
const reactRoot = window.document.getElementById("root");
|
||||
initialize().then(({ provider }) => {
|
||||
ReactDOM.render(provider, reactRoot);
|
||||
});
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
import React from "react";
|
||||
import { connect } from 'react-redux';
|
||||
import Spinner from "react-loader";
|
||||
import * as BS from "react-bootstrap";
|
||||
// import * as BS from "react-bootstrap";
|
||||
import * as A from '../actions/entities';
|
||||
import { Route, IndexRoute, Link, IndexLink } from "react-router";
|
||||
|
||||
|
||||
// import { Money } from '../components/Money';
|
||||
@@ -32,15 +33,15 @@ export class AccountInfo extends React.Component {
|
||||
const account = entities[accountId];
|
||||
|
||||
if (!account) {
|
||||
return (<div>{ accountId } <Spinner ref="spinner" loaded={false} /></div>)
|
||||
return (<Link to={ `/account/${accountId}` }>{ accountId } <Spinner loaded={false} /></Link>)
|
||||
}
|
||||
|
||||
const { title } = account;
|
||||
|
||||
return (<div>{ title } </div>);
|
||||
return (<Link to={ `/account/${accountId}` }>{ title }</Link>);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ app }) => ({
|
||||
entities: app.data.entities
|
||||
}))(AccountInfo);
|
||||
}))(AccountInfo);
|
||||
|
||||
@@ -11,49 +11,99 @@ import AccountInfo from './AccountInfo';
|
||||
|
||||
export class TransfersTable extends React.Component {
|
||||
render() {
|
||||
const { loading, data, errors } = this.props;
|
||||
|
||||
if (loading) {
|
||||
return (<h2><Spinner ref="spinner" loaded={false} /> Loading..</h2>);
|
||||
const { transfers, forAccount } = this.props;
|
||||
const { loading, data, errors } = transfers || {};
|
||||
|
||||
if (!transfers || loading) {
|
||||
return (<h2><Spinner loaded={false} /> Loading..</h2>);
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length) {
|
||||
return (<div className="text-danger">Errors..</div>);
|
||||
}
|
||||
|
||||
const transfers = data.length ? data.map(({
|
||||
amount,
|
||||
fromAccountId,
|
||||
toAccountId,
|
||||
transactionId,
|
||||
description = '',
|
||||
date = null,
|
||||
status = ''
|
||||
}, idx) => (<tr key={idx}>
|
||||
<td><TimeAgo date={date} /></td>
|
||||
<td><AccountInfo accountId={ fromAccountId } /></td>
|
||||
<td><AccountInfo accountId={ toAccountId } /></td>
|
||||
<td><Money amount={ amount } /></td>
|
||||
<td>{ description || 'N/a'}</td>
|
||||
<td>{ status || 'N/a' }</td>
|
||||
</tr>)) : (<tr>
|
||||
<td colSpan={6}>No transfers for this account just yet.</td>
|
||||
</tr>);
|
||||
const currentAccountId = forAccount;
|
||||
const transfersMarkup = data.length ?
|
||||
data
|
||||
.sort((a, b) => ((a.date - b.date)))
|
||||
.filter(({ entryType, toAccountId, fromAccountId}) => ((entryType !=='transaction') || (fromAccountId === currentAccountId) || (toAccountId === currentAccountId)))
|
||||
.reduce(({
|
||||
items, balance
|
||||
}, v) => {
|
||||
if (v.entryType == 'account') {
|
||||
balance = v.initialBalance;
|
||||
} else if (v.entryType == 'transaction') {
|
||||
const isOriginating = v.fromAccountId == currentAccountId;
|
||||
balance += (isOriginating ? -1 : 1) * v.amount;
|
||||
}
|
||||
v.balance = balance;
|
||||
items.push(v);
|
||||
return { items, balance };
|
||||
}, {
|
||||
items: [],
|
||||
balance: 0
|
||||
}).items
|
||||
.sort((a, b) => (-(a.date - b.date)))
|
||||
.map(({
|
||||
entryType,
|
||||
amount,
|
||||
fromAccountId,
|
||||
toAccountId,
|
||||
transactionId,
|
||||
description = '—',
|
||||
date = null,
|
||||
status = '—',
|
||||
balance,
|
||||
initialBalance = null
|
||||
}) => {
|
||||
|
||||
const transferTimestamp = new Date(date);
|
||||
const timeAgoTitle = transferTimestamp.toLocaleDateString() + ' ' + transferTimestamp.toLocaleTimeString();
|
||||
|
||||
if (entryType == 'account') {
|
||||
return (<tr>
|
||||
<td><TimeAgo date={date} title={ timeAgoTitle } /></td>
|
||||
<td colSpan="3">Account created</td>
|
||||
<td><Money amount={ initialBalance } /></td>
|
||||
<td></td>
|
||||
<td>{ status || '—' }</td>
|
||||
</tr>);
|
||||
}
|
||||
|
||||
const isOriginating = fromAccountId == currentAccountId;
|
||||
const directionMarkup = isOriginating ? 'Debit' : 'Credit';
|
||||
const counterAccountMarkup = isOriginating ?
|
||||
<AccountInfo accountId={ toAccountId } /> :
|
||||
<AccountInfo accountId={ fromAccountId } />;
|
||||
|
||||
return (<tr>
|
||||
<td><TimeAgo date={date} title={ timeAgoTitle } /></td>
|
||||
<td>{ directionMarkup }</td>
|
||||
<td>{ counterAccountMarkup }</td>
|
||||
<td><Money amount={ amount } /></td>
|
||||
<td><Money amount={ balance } /></td>
|
||||
<td>{ description || '—' }</td>
|
||||
<td>{ status || '—' }</td>
|
||||
</tr>);
|
||||
}) : (<tr>
|
||||
<td colSpan={6}>No transfers for this account just yet.</td>
|
||||
</tr>);
|
||||
|
||||
return (
|
||||
<BS.Table striped bordered condensed hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>What</th>
|
||||
<th>Counter Account</th>
|
||||
<th>Type</th>
|
||||
<th>Other Account</th>
|
||||
<th>Amount</th>
|
||||
<th>Balance</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ transfers }
|
||||
{ transfersMarkup }
|
||||
</tbody>
|
||||
</BS.Table>
|
||||
);
|
||||
|
||||
@@ -11,11 +11,6 @@ import HeaderLinks from '../HeaderLinks';
|
||||
|
||||
//const SignOutButton = () => (<div>SignOutButton!</div>);
|
||||
|
||||
|
||||
//if (!global.__SERVER__ && !global.__TEST__) {
|
||||
// require("../../styles/main.scss");
|
||||
//}
|
||||
|
||||
class Container extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
import defineActionTypes from '../utils/defineActionTypes'
|
||||
import { TODO_DEFINE, defineActionType } from '../utils/defineActionTypes'
|
||||
|
||||
export default defineActionTypes({
|
||||
export default defineActionType({
|
||||
/*
|
||||
* View model
|
||||
*/
|
||||
AUTH: `
|
||||
CONFIGURE_START
|
||||
CONFIGURE_COMPLETE
|
||||
CONFIGURE_ERROR
|
||||
AUTHENTICATE_START
|
||||
AUTHENTICATE_COMPLETE
|
||||
AUTHENTICATE_ERROR
|
||||
SIGN_IN_START
|
||||
SIGN_IN_COMPLETE
|
||||
SIGN_IN_ERROR
|
||||
SIGN_IN_FORM_UPDATE
|
||||
SIGN_UP_START
|
||||
SIGN_UP_COMPLETE
|
||||
SIGN_UP_ERROR
|
||||
SIGN_UP_FORM_UPDATE
|
||||
SIGN_OUT_START
|
||||
SIGN_OUT_COMPLETE
|
||||
`,
|
||||
AUTH: {
|
||||
CONFIGURE_START: TODO_DEFINE,
|
||||
CONFIGURE_COMPLETE: TODO_DEFINE,
|
||||
CONFIGURE_ERROR: TODO_DEFINE,
|
||||
AUTHENTICATE_START: TODO_DEFINE,
|
||||
AUTHENTICATE_COMPLETE: TODO_DEFINE,
|
||||
AUTHENTICATE_ERROR: TODO_DEFINE,
|
||||
SIGN_IN_START: TODO_DEFINE,
|
||||
SIGN_IN_COMPLETE: TODO_DEFINE,
|
||||
SIGN_IN_ERROR: TODO_DEFINE,
|
||||
SIGN_IN_FORM_UPDATE: TODO_DEFINE,
|
||||
SIGN_UP_START: TODO_DEFINE,
|
||||
SIGN_UP_COMPLETE: TODO_DEFINE,
|
||||
SIGN_UP_ERROR: TODO_DEFINE,
|
||||
SIGN_UP_FORM_UPDATE: TODO_DEFINE,
|
||||
SIGN_OUT_START: TODO_DEFINE,
|
||||
SIGN_OUT_COMPLETE: TODO_DEFINE
|
||||
},
|
||||
|
||||
ENTITIES: `
|
||||
REQUESTED
|
||||
RECEIVED
|
||||
RECEIVED_LIST
|
||||
`,
|
||||
ENTITIES: {
|
||||
REQUESTED: TODO_DEFINE,
|
||||
RECEIVED: TODO_DEFINE,
|
||||
RECEIVED_LIST: TODO_DEFINE
|
||||
},
|
||||
|
||||
ACCOUNTS: `
|
||||
LIST_START
|
||||
LIST_COMPLETE
|
||||
LIST_ERROR
|
||||
LIST_REF_START
|
||||
LIST_REF_COMPLETE
|
||||
LIST_REF_ERROR
|
||||
CREATE_START
|
||||
CREATE_COMPLETE
|
||||
CREATE_ERROR
|
||||
CREATE_FORM_UPDATE
|
||||
EDIT_START
|
||||
EDIT_COMPLETE
|
||||
EDIT_ERROR
|
||||
EDIT_FORM_UPDATE
|
||||
CREATE_REF_START
|
||||
CREATE_REF_COMPLETE
|
||||
CREATE_REF_ERROR
|
||||
CREATE_REF_FORM_UPDATE
|
||||
CREATE_REF_OWNER_LOOKUP_START
|
||||
CREATE_REF_OWNER_LOOKUP_COMPLETE
|
||||
CREATE_REF_ACCOUNT_LOOKUP_START
|
||||
CREATE_REF_ACCOUNT_LOOKUP_COMPLETE
|
||||
`,
|
||||
ACCOUNTS: {
|
||||
LIST_START: TODO_DEFINE,
|
||||
LIST_COMPLETE: TODO_DEFINE,
|
||||
LIST_ERROR: TODO_DEFINE,
|
||||
LIST_REF_START: TODO_DEFINE,
|
||||
LIST_REF_COMPLETE: TODO_DEFINE,
|
||||
LIST_REF_ERROR: TODO_DEFINE,
|
||||
CREATE_START: TODO_DEFINE,
|
||||
CREATE_COMPLETE: TODO_DEFINE,
|
||||
CREATE_ERROR: TODO_DEFINE,
|
||||
CREATE_FORM_UPDATE: TODO_DEFINE,
|
||||
EDIT_START: TODO_DEFINE,
|
||||
EDIT_COMPLETE: TODO_DEFINE,
|
||||
EDIT_ERROR: TODO_DEFINE,
|
||||
EDIT_FORM_UPDATE: TODO_DEFINE,
|
||||
CREATE_REF_START: TODO_DEFINE,
|
||||
CREATE_REF_COMPLETE: TODO_DEFINE,
|
||||
CREATE_REF_ERROR: TODO_DEFINE,
|
||||
CREATE_REF_FORM_UPDATE: TODO_DEFINE,
|
||||
CREATE_REF_OWNER_LOOKUP_START: TODO_DEFINE,
|
||||
CREATE_REF_OWNER_LOOKUP_COMPLETE: TODO_DEFINE,
|
||||
CREATE_REF_ACCOUNT_LOOKUP_START: TODO_DEFINE,
|
||||
CREATE_REF_ACCOUNT_LOOKUP_COMPLETE: TODO_DEFINE
|
||||
},
|
||||
|
||||
ACCOUNT: `
|
||||
SINGLE_START
|
||||
SINGLE_COMPLETE
|
||||
SINGLE_ERROR
|
||||
DELETE_START
|
||||
DELETE_COMPLETE
|
||||
DELETE_ERROR
|
||||
`,
|
||||
ACCOUNT: {
|
||||
SINGLE_START: TODO_DEFINE,
|
||||
SINGLE_COMPLETE: TODO_DEFINE,
|
||||
SINGLE_ERROR: TODO_DEFINE,
|
||||
DELETE_START: TODO_DEFINE,
|
||||
DELETE_COMPLETE: TODO_DEFINE,
|
||||
DELETE_ERROR: TODO_DEFINE
|
||||
},
|
||||
|
||||
TRANSFERS: `
|
||||
MAKE_START
|
||||
MAKE_COMPLETE
|
||||
MAKE_ERROR
|
||||
MAKE_FORM_UPDATE
|
||||
LIST_START
|
||||
LIST_COMPLETE
|
||||
LIST_ERROR
|
||||
`,
|
||||
TRANSFERS: {
|
||||
MAKE_START: TODO_DEFINE,
|
||||
MAKE_COMPLETE: TODO_DEFINE,
|
||||
MAKE_ERROR: TODO_DEFINE,
|
||||
MAKE_FORM_UPDATE: TODO_DEFINE,
|
||||
LIST_START: TODO_DEFINE,
|
||||
LIST_COMPLETE: TODO_DEFINE,
|
||||
LIST_ERROR: TODO_DEFINE
|
||||
},
|
||||
|
||||
ERROR: `
|
||||
START
|
||||
STOP
|
||||
`
|
||||
ERROR: {
|
||||
START:TODO_DEFINE,
|
||||
STOP:TODO_DEFINE
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user