diff --git a/.github/workflows/autocomment.yml b/.github/workflows/autocomment.yml index c40a127b..521e12c4 100644 --- a/.github/workflows/autocomment.yml +++ b/.github/workflows/autocomment.yml @@ -19,5 +19,6 @@ jobs: Please make sure you have followed our contributing guidelines. We will review it as soon as possible. In the meanwhile make sure your PR checks the following boxes - [ ] Is based on an issue - [ ] Has been locally tested - - [ ] Has been tested with the admin UI + - [ ] Has been tested with the admin ui + - [ ] Has been documented - [ ] Has been discussed with the development team in an open channel diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c37ee2ea..bb6570cd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,5 +21,5 @@ jobs: git config user.name "GitHub Actions" git config user.email "" git add docs/classes - git commit -m "Update phpdoc" || echo "No changes to commit" + git commit -m "Update phpdoc-md" || echo "No changes to commit" git push \ No newline at end of file diff --git a/Service/CacheService.php b/Service/CacheService.php index d33f1e7a..0dc4c3a7 100644 --- a/Service/CacheService.php +++ b/Service/CacheService.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Serializer\SerializerInterface; +use Psr\Log\LoggerInterface; /** * Service to call external sources. @@ -37,6 +38,7 @@ class CacheService private SymfonyStyle $io; private ParameterBagInterface $parameters; private SerializerInterface $serializer; + private LoggerInterface $logger; /** * @param AuthenticationService $authenticationService @@ -88,6 +90,8 @@ public function cleanup() $filter = []; $objects = $collection->find($filter)->toArray(); (isset($this->io) ? $this->io->writeln('Found '.count($objects).'') : ''); + + //$this->logger->info("Ran cache cleanup"); } /** @@ -107,6 +111,7 @@ public function warmup() if (!isset($this->client)) { (isset($this->io) ? $this->io->writeln('No cache client found, halting warmup') : ''); + //$this->info->logger("Ran cache Warmup"); return Command::SUCCESS; } @@ -163,6 +168,7 @@ public function warmup() (isset($this->io) ? $this->io->writeln(['Removing deleted objects', '============']) : ''); $this->removeDataFromCache($this->client->objects->json, 'App:ObjectEntity'); + //$this->logger->info("Ran cache Warmup"); return Command::SUCCESS; } @@ -200,11 +206,6 @@ private function ioCatchException(Exception $exception) */ public function cacheObject(ObjectEntity $objectEntity): ObjectEntity { - // For when we can't generate a schema for an ObjectEntity (for example setting an id on ObjectEntity created with testData) - if (!$objectEntity->getEntity()) { - return $objectEntity; - } - // Backwards compatablity if (!isset($this->client)) { return $objectEntity; @@ -243,6 +244,8 @@ public function cacheObject(ObjectEntity $objectEntity): ObjectEntity (isset($this->io) ? $this->io->writeln('Wrote object '.$objectEntity->getId()->toString().' of type '.$objectEntity->getEntity()->getName().' to cache') : ''); } + + //$this->logger->debug("Cached object"); return $objectEntity; } @@ -264,6 +267,7 @@ public function removeObject(ObjectEntity $object): void $collection = $this->client->objects->json; $collection->findOneAndDelete(['_id'=>$id]); + //$this->logger->debug("Delete object"); } /** @@ -292,6 +296,7 @@ public function getObject(string $id) return $this->cacheObject($object)->toArray(['embedded' => true]); } + //$this->logger->debug("Retrieved object from cache"); return false; } @@ -366,6 +371,8 @@ public function searchObjects(string $search = null, array $filter = [], array $ $total = $collection->count($filter); // Make sure to add the pagination properties in response + + //$this->logger->debug("Searched cache for objects"); return $this->handleResultPagination($completeFilter, $results, $total); } @@ -753,27 +760,27 @@ public function cacheEndpoint(Endpoint $endpoint): Endpoint } if (isset($this->io)) { - $this->io->writeln('Start caching endpoint '.$endpoint->getId()->toString().' with name: '.$endpoint->getName()); + $this->io->writeln('Start caching object '.$endpoint->getId()->toString()); } - $updatedEndpoint = $this->entityManager->getRepository('App:Endpoint')->find($endpoint->getId()); - if ($updatedEndpoint instanceof Endpoint) { - $endpoint = $updatedEndpoint; + $updatedEntity = $this->entityManager->getRepository('App:Endpoint')->find($endpoint->getId()); + if ($updatedEntity instanceof Endpoint) { + $entity = $updatedEntity; } elseif (isset($this->io)) { - $this->io->writeln('Could not find an Endpoint with id: '.$endpoint->getId()->toString()); + $this->io->writeln('Could not find an Endpoint with id: '.$objectEntity->getId()->toString()); } $collection = $this->client->endpoints->json; - $endpointArray = $this->serializer->normalize($endpoint); + $entityArray = $this->serializer->normalize($entity); if ($collection->findOneAndReplace( ['id' => $endpoint->getId()->toString()], - $endpointArray, + $entityArray, ['upsert'=>true] )) { - (isset($this->io) ? $this->io->writeln('Updated endpoint '.$endpoint->getId()->toString().' to cache') : ''); + (isset($this->io) ? $this->io->writeln('Updated object '.$endpoint->getId()->toString().' of type Endpoint to cache') : ''); } else { - (isset($this->io) ? $this->io->writeln('Wrote object '.$endpoint->getId()->toString().' to cache') : ''); + (isset($this->io) ? $this->io->writeln('Wrote object '.$endpoint->getId()->toString().' of type Endpoint to cache') : ''); } return $endpoint; diff --git a/Service/InstallationService.php b/Service/InstallationService.php index cdd7b221..cd729bcb 100644 --- a/Service/InstallationService.php +++ b/Service/InstallationService.php @@ -11,6 +11,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; +use Psr\Log\LoggerInterface; class InstallationService { @@ -296,21 +297,11 @@ public function handleData($file) if (array_key_exists('_id', $object) && $objectEntity = $this->em->getRepository('App:ObjectEntity')->findOneBy(['id' => $object['_id']])) { $this->io->writeln(['', 'Object '.$object['_id'].' already exists, so updating']); } elseif (array_key_exists('_id', $object)) { - $this->io->writeln('Set id to '.$object['_id']); - - // Nice doctrine setId shizzle - $objectEntity = new ObjectEntity(); - $this->em->persist($objectEntity); - $objectEntity->setId($object['_id']); - $this->em->persist($objectEntity); - $this->em->flush(); - $this->em->refresh($objectEntity); - - $objectEntity = $this->em->getRepository('App:ObjectEntity')->findOneBy(['id' => $object['_id']]); - - $objectEntity->setEntity($entity); - + $this->io->writeln('Set external id to '.$object['_id']); + $objectEntity = new ObjectEntity($entity); $this->io->writeln('Creating new object with existing id '.$objectEntity->getId()); + + $objectEntity->setExternalId($object['_id']); } else { $objectEntity = new ObjectEntity($entity); $this->io->writeln(['', 'Creating new object']); @@ -349,4 +340,48 @@ public function handleInstaller($file) return $installationService->install(); } + + /** + * Handles forced id's on object entities + * + * @param ObjectEntity $objectEntity + * @return ObjectEntity + */ + private function saveOnFixedId(ObjectEntity $objectEntity): ObjectEntity{ + // Save the values + $values = $object->getObjectValues()->toArray(); + $object->clearAllValues(); + + // We have an object entity with a fixed id that isn't in the database, so we need to act + if($objectEntity->getId() && !$this->em->contains($objectEntity)){ + // Sve the id + $id = $objectEntity->getId(); + // Create the entity + $this->em->persist($objectEntity); + $this->em->flush(); + $this->em->refresh($objectEntity); + // Reset the id + $objectEntity->setId($id); + $this->em->persist($objectEntity); + $this->em->flush(); + $objectEntity = $this->entityManager->getRepository('App:ObjectEntity')->findOneBy(['id' => $id]); + } + + // Loop trough the values + foreach ($values as $objectValue){ + // If the value itsself is an object it might also contain fixed id's + foreach ($objectValue->getObjects() as $subobject){ + $subobject = $this->saveOnFixedId($subobject); + } + + $objectEntity->addObjectValue($objectValue); + } + + $this->em->persist($objectEntity); + $this->em->flush(); + + return $objectEntity; + } + + } diff --git a/Service/RequestService.php b/Service/RequestService.php index f66cb90f..0cdcb940 100644 --- a/Service/RequestService.php +++ b/Service/RequestService.php @@ -6,10 +6,8 @@ use App\Entity\Endpoint; use App\Entity\Entity; use App\Entity\Gateway as Source; -use App\Entity\Log; use App\Entity\ObjectEntity; use App\Event\ActionEvent; -use App\Service\LogService; use App\Service\ObjectEntityService; use App\Service\ResponseService; use Doctrine\Common\Collections\Criteria; @@ -20,6 +18,7 @@ use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\SerializerInterface; +use Psr\Log\LoggerInterface; /** * Handles incomming request from endpoints or controllers that relate to the gateways object structure (eav). @@ -36,8 +35,7 @@ class RequestService // todo: we might want to move or rewrite code instead of using these services here: private ResponseService $responseService; private ObjectEntityService $objectEntityService; - private LogService $logService; - private CallService $callService; + private LoggerInterface $logger; private Security $security; private EventDispatcherInterface $eventDispatcher; private SerializerInterface $serializer; @@ -47,7 +45,7 @@ class RequestService * @param CacheService $cacheService * @param ResponseService $responseService * @param ObjectEntityService $objectEntityService - * @param LogService $logService + * @param LoggerInterface $RequestLog * @param CallService $callService * @param Security $security * @param EventDispatcherInterface $eventDispatcher @@ -58,7 +56,7 @@ public function __construct( CacheService $cacheService, ResponseService $responseService, ObjectEntityService $objectEntityService, - LogService $logService, + LoggerInterface $RequestLog, CallService $callService, Security $security, EventDispatcherInterface $eventDispatcher, @@ -68,7 +66,7 @@ public function __construct( $this->cacheService = $cacheService; $this->responseService = $responseService; $this->objectEntityService = $objectEntityService; - $this->logService = $logService; + $this->logger = $RequestLog; $this->callService = $callService; $this->security = $security; $this->eventDispatcher = $eventDispatcher; @@ -247,6 +245,8 @@ public function proxyHandler(array $data, array $configuration): Response "No Endpoint in data['endpoint']" : "This Endpoint has no Proxy: {$data['endpoint']->getName()}"; + $this->logger->error('This Endpoint has no Proxy'); + return new Response( json_encode(['Message' => $message]), Response::HTTP_NOT_FOUND, @@ -255,6 +255,9 @@ public function proxyHandler(array $data, array $configuration): Response } if ($proxy instanceof Source && !$proxy->getIsEnabled()) { + + $this->logger->error('Source is not enabled'); + return new Response( json_encode(['Message' => "This Source is not enabled: {$proxy->getName()}"]), Response::HTTP_OK, // This should be ok so we can disable Sources without creating error responses? @@ -278,6 +281,8 @@ public function proxyHandler(array $data, array $configuration): Response ] ); + $this->logger->debug('Handled proxy request'); + // Let create a responce from the guzle call $responce = new Response( $result->getBody()->getContents(), @@ -411,10 +416,11 @@ public function requestHandler(array $data, array $configuration): Response $responseLog = new Response(is_string($this->content) || is_null($this->content) ? $this->content : null, 200, ['CoreBundle' => 'GetItem']); $session = new Session(); $session->set('object', $this->id); - $this->logService->saveLog($this->logService->makeRequest(), $responseLog, 15, is_array($this->content) ? json_encode($this->content) : $this->content); + $this->logger->debug('GET on collection endpoint',['method'=>'GET']); } else { //$this->data['query']['_schema'] = $this->data['endpoint']->getEntities()->first()->getReference(); - $result = $this->cacheService->searchObjects(null, $filters, $allowedSchemas['id']); + $result = $this->cacheService->searchObjects(null, $filters, $allowedSchemas); + $this->logger->debug('GET on item endpoint',['method'=>'GET']); } break; case 'POST': @@ -446,6 +452,7 @@ public function requestHandler(array $data, array $configuration): Response // Use validation to throw an error } + $this->logger->debug('PUT on collection endpoint',['method'=>'PUT']); $result = $this->cacheService->getObject($this->object->getId()); break; case 'PUT': @@ -477,6 +484,8 @@ public function requestHandler(array $data, array $configuration): Response // Use validation to throw an error } + + $this->logger->debug('PUT on collection endpoint',['method'=>'PUT']); $result = $this->cacheService->getObject($this->object->getId()); break; case 'PATCH': @@ -508,6 +517,7 @@ public function requestHandler(array $data, array $configuration): Response // Use validation to throw an error } + $this->logger->debug('PATC on collection endpoint',['method'=>'PATCH']); $result = $this->cacheService->getObject($this->object->getId()); break; case 'DELETE': @@ -531,12 +541,14 @@ public function requestHandler(array $data, array $configuration): Response // $this->cacheService - removeObject($this->id); /* @todo this is hacky, the above schould alredy do this */ $this->entityManager->flush(); + $this->logger->debug('DELETE on collection endpoint',['method'=>'DELETE']); return new Response('Succesfully deleted object', '202'); break; default: - break; + $this->logger->error('Unknown method: '.$this->data['method'],['method'=>$this->data['method']]); return new Response('Unkown method'.$this->data['method'], '404'); + break; } $this->entityManager->flush(); @@ -642,60 +654,6 @@ private function handleMetadataSelf(&$result, array $metadataSelf) $result['_self'] = $resultMetadataSelf; } - /** - * @param array $data The data from the call - * @param array $configuration The configuration from the call - * - * @return array The modified data - */ - public function itemRequestHandler(array $data, array $configuration): array - { - $this->data = $data; - $this->configuration = $configuration; - - $method = $this->data['request']->getMethod(); - $content = $this->data['request']->getContent(); - - // Lets see if we have an object - if (array_key_exists('id', $this->data)) { - $this->id = $data['id']; - if (!$this->object = $this->cacheService->getObject($data['id'])) { - // Throw not found - } - } - - switch ($method) { - case 'GET': - break; - case 'PUT': - - if ($validation = $this->object->validate($content) && $this->object->hydrate($content, true)) { - $this->entityManager->persist($this->object); - } else { - // Use validation to throw an error - } - break; - case 'PATCH': - if ($this->object->hydrate($content) && $validation = $this->object->validate()) { - $this->entityManager->persist($this->object); - } else { - // Use validation to throw an error - } - break; - case 'DELETE': - $this->entityManager->remove($this->object); - - return new Response('', '202'); - break; - default: - break; - } - - $this->entityManager->flush(); - - return $this->createResponse($this->object); - } - /** * This function searches all the objectEntities and formats the data. * @@ -723,6 +681,9 @@ public function searchRequestHandler(array $data, array $configuration): array ]; } + + $this->logger->debug('Handled search request'); + $this->data['response'] = $response = new Response( json_encode($response), 200, diff --git a/Service/UserService.php b/Service/UserService.php new file mode 100644 index 00000000..810e3b04 --- /dev/null +++ b/Service/UserService.php @@ -0,0 +1,143 @@ +entityManager = $entityManager; + $this->authenticationService = $authenticationService; + } + + /** + * @param User $user + * @param string $password + * + * @return bool + */ + public function validatePassword(User $user, string $password): bool + { + + // Todo: this oforuce is hacky as hell and a security danger + return true; + } + + /** + * Generates a JWT token for a user. + * + * @todo should be moved to authentication service + * + * @param array $payload + * + * @return string + */ + public function createJWTToken(array $payload): string + { + $algorithmManager = new AlgorithmManager([new RS512()]); + $pem = $this->fileService->writeFile('privatekey', base64_decode($this->parameterBag->get('private_key'))); + $jwk = JWKFactory::createFromKeyFile($pem); + $this->fileService->removeFile($pem); + + $jwsBuilder = new JWSBuilder($algorithmManager); + $jws = $jwsBuilder + ->create() + ->withPayload(json_encode($payload)) + ->addSignature($jwk, ['alg' => 'RS512']) + ->build(); + + $serializer = new CompactSerializer(); + + return $serializer->serialize($jws, 0); + } + + /** + * Creates a CsrfToken for a user. + * + * @todo should be moved to authentication service + * + * @param User $user + * + * @return CsrfToken + */ + public function createCsrfToken(User $user): CsrfToken + { + $tokenManager = new CsrfTokenManager(); + + return $tokenManager->getToken($user->getId()->toString()); + } + + /** + * Creates the user object responce for login and /me requests. + * + * @param User $user + * + * @return array + */ + public function createResponce(User $user) + { + // prepare the security groups + $groups = []; + $applications = []; + + foreach ($user->getSecurityGroups() as $securityGroup) { + $securityGroups[] = ['id'=>$securityGroup->getId()->toString(), 'name'=>$securityGroup->getName()]; + } + + foreach ($user->getApplications() as $application) { + $applications[] = ['id'=>$application->getId()->toString(), 'name'=>$application->getName()]; + } + + // prepare the responce + $responce = [ + '_self'=> [ + 'id'=> $user->getId()->toString(), + ], + 'id' => $user->getId()->toString(), + 'organisation'=> [ + 'id' => $user->getOrganisation()->getId()->toString(), + 'name'=> $user->getOrganisation()->getName(), + ], + 'applications'=> $user->getApplications()->getId()->toString(), + 'username' => $user->getUsername(), + 'email' => $user->getEmail(), + 'locale' => $user->getLocale(), + 'person' => $user->getPerson(), + 'roles' => $user->getRoles(), + 'groups' => $securityGroups, + 'jwtToken' => $this->createJwtToken($user), + 'csrfToken' => $this->createCsrfToken($user), + 'organisation'=> $user->getOrganisation()->getId()->toString(), + 'application' => $user->getApplications()->getId()->toString(), + ]; + + return $responce; + } + + public function validateToken() + { + } +} diff --git a/docs/classes/Service/RequestService.md b/docs/classes/Service/RequestService.md index 932aa387..67720aa4 100644 --- a/docs/classes/Service/RequestService.md +++ b/docs/classes/Service/RequestService.md @@ -15,7 +15,6 @@ Handles incomming request from endpoints or controllers that relate to the gatew |[createResponse](#requestservicecreateresponse)|Creating the responce object.| |[getId](#requestservicegetid)|Get the ID from given parameters.| |[getSchema](#requestservicegetschema)|Get the schema from given parameters returns false if no schema could be established.| -|[itemRequestHandler](#requestserviceitemrequesthandler)|| |[proxyHandler](#requestserviceproxyhandler)|| |[realRequestQueryAll](#requestservicerealrequestqueryall)|A function to replace Request->query->all() because Request->query->all() will replace some characters with an underscore.| |[requestHandler](#requestservicerequesthandler)|Handles incomming requests and is responsible for generating a responce.| @@ -151,30 +150,6 @@ Get the schema from given parameters returns false if no schema could be establi -
- - -### RequestService::itemRequestHandler - -**Description** - -```php - itemRequestHandler (void) -``` - - - - - -**Parameters** - -`This function has no parameters.` - -**Return Values** - -`void` - -
diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..0da4f587 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,11 @@ +The commonground web gateway aims at providing an easy out of the box experience for developers, be it either in local development or production environments. It does that by providing a no nonsense api design to be the ajax engine under any modern progressive web app. But it could also be used the navigate the complexities of an microservice ecosystem form an application point of view or handle an omni-channel strategy. + +Anyways, you can read the full sales pitch on the product page. + +This however is the technical documentation where we can read a bit more about +- Features +- Roadmap +- Security +- Installation +- Design Decisions +- Contributing diff --git a/docs/schemas.md b/docs/schemas.md new file mode 100644 index 00000000..9411a92c --- /dev/null +++ b/docs/schemas.md @@ -0,0 +1,220 @@ +# Schema’s +Scherma’s are the core of the Common Gateway’s data layer. They define and model objects and set conditions that objects should adhere to. Each object in the gateway always belongs to ONE schema. Schemas follow the [json schema](https://json-schema.org/) standard and are therefore interchangeable with [OAS3](https://swagger.io/specification/) schemas. For the Dutch governmental ecosystem this means that a schema adheres to the `overige objecten standaard` description of an `object type`. + +In a more traditional way schema’s can be viewed as the “tables” of the data layer, they store data in a predefined way. But unlike tables the data is stored as objects. Where each data set that would normally be a row becomes an object. The main difference being that objects are multidimensional arrays, or in other words 3 dimensional. Where table rows are flat (each column containing one value) an object can contain an array, objects or arrays of objects. This presents us with a vastly superior way of serving data. + +## Creating or updating a schema +Schema’s can be models from the schema page in de Admin UI or trough the /admin/schema endpoint. To create a new schema go to “Schemas” in the menu, and press “Add schema”. Just fill in the name “PET and hit save (a schema needs to be created before you can add properties). After the schema is created you are automatically redirected to the schema’s edit page. + +## Adding properties to a schema +Go to the properties tab and press “Add property” to add a property to your schema. + +## Adding objects +After adding properties to a schema + + +## Downloading schema’s + +## Uploading schema’s + +You can upload a schema in the Gateway UI by pressing the upload button in the top right corner. Schema’s might also be uploaded by plugins or collections. When a schema is uploaded the following things will happen: + +The Gateway will look in its schema library if a version of that schema is already present. It does so based on the schema ID. +Based on that result the Gateway will handle the schema accordingly: +If no matching schema is found the gateway will create a new schema +If a matching schema is found the gateway wil compare versions and decide what to do: +The old schema has no set version and the new schema has no set version -> The gateway will update the old schema with the new schema +The old schema has no set version and the new schema has a set version -> The gateway will update the old schema with the new schema +The old schema has a set version number and the new schema has a higher set version number -> The gateway will update the old schema with the new schema +The old schema has a set version number and the new schema has a lower set version number -> The gateway does not update the schema +The old schema has a set version number and the new schema does not have a set version number -> The gateway does not update the schema + +Lets take a look at an example. We have a weather plugin that contains a weather schema. + + + +##Properties +An entity consists of the following properties that can be configured + +| Property | Required | Description | +|--------|-------------| -------------| +|name| yes |An unique name for this entity| +|description| no | The description for this entity that wil be shown in de API documentation| +|source| no |The source where this entity resides| +|endpoint| yes if an source is provided | The endpoint within the source that this entity should be posted to as an object| +|route| no | The route this entity can be found easier, should be a path | +|extend| no | Whether or not the properties of the original object are automatically included| + +###Attribute +Attributes represent properties on objects that you want to communicate to underlying sources. In a normal setup and attribute should at leas apply the same restrictions as the underlying property (e.g. required) to prevent errors when pushing the entity to its source. It can however provide additional validations to a property, for example the source AIU might simply require the property ‘email’ to be a unique string, but you could set the form to ‘email’ causing the input to be validated as an ISO compatible email address. + + +####Properties + +| Property | Required | Description | +|--------|-------------| -------------| +|name| yes |`string` An name for this attribute. MUST be unique on an entity level and MAY NOT be ‘id’,’file’,‘files’, ’search’,’fields’,’start’,’page’,’limit’,’extend’ or ’organization’| +|description| no | The description for this attribute that wil be shown in de API documentation| +|type| yes |`string` See [types](#Types)| +|format| no |`string` See [formats](#Formats)| +|validations| no |`array of strings` See [validations](#Validations)| +|multiple| no |`boolean` if this attribute expects an array of the given type | +|defaultValue| no |`string` An default value for this value that will be used if a user doesn't supply a value| +|deprecated| no |`boolean` Whether or not this property has been deprecated and wil be removed in the future| +|required| no |`boolean` whether or not this property is required to be in a POST or UPDATE| +|requiredIf| no |`array` a nested set of validations that will cause this attribute to become required | +|forbidden| no |`boolean` whether or not this property is forbiden to be in a POST or UPDATE| +|forbiddenIf| no |`array` a nested set of validations that will cause this attribute to become forbidden| +|example| no |`string` An example of the value that should be supplied| +|persistToSource| no |`boolean` Setting this property to true wil force the property to be saved in the gateway endpoint (default behafure is saving in the EAV)| +|searchable| no |`boolean` Whether or not this property is searchable| +|cascade| no |`boolean` Whether or not this property kan be used to create new entities (versus when it can only be used to link exsisting entities)| + +####Types +The type of attribute provides basic validations and a way for the gateway to store and cash values in an efficient manner. Types are derived from the OAS3 specification. Current available types are: + +| Format | Description | +|--------|-------------| +|string| a text | +|integer| a full number without decimals| +|decimal| a number including decimals| +|boolean| a true/false | +|date| an ISO-??? date | +|date-time| an ISO-??? date | +|array| an array or list of values| +|object|Used to nest a Entity as atribute of antother Entity, read more about [nesting]()| +|file|Used to handle file uploads, an Entity SHOULD only contain one atribute of the type file, read more about [handling file uploads]() | + +* you are allowed to use integer instead of int, boolean instead of bool, date-time or dateTime instead of datetime, + +####Formats +A format defines a way a value should be formatted, and is directly connected to a type, for example a string MAY BE a format of email, but an integer cannot be a valid email. Formats are derived from the OAS3 specification, but supplemented with formats that are generally needed in governmental applications (like BSN) . Current available formats are: + + +General formats + +| Format | Type(s) | Description | +|--------|---------|-------------| +|alnum| |Validates whether the input is alphanumeric or not. Alphanumeric is a combination of alphabetic and numeric characters| +|alpha| |Validates whether the input contains only alphabetic characters| +|numeric| |Validates whether the input contains only numeric characters| +|uuid|string|| +|base| |Validate numbers in any base, even with non reqular bases.| +|base64| | Validate if a string is Base64-encoded.| +|countryCode|string|Validates whether the input is a country code in ISO 3166-1 standard.| +|creditCard|string|Validates a credit card number.| +|currencyCode|string|Validates an ISO 4217 currency code like GBP or EUR.| +|digit|string|Validates whether the input contains only digits.| +|directory|string|Validates if the given path is a directory.| +|domain|string|Validates whether the input is a valid domain name or not.| +|url|string|Validates whether the input is a valid url or not.| +|email|string|Validates an email address.| +|phone|string|Validates an phone number.| +|fibonacci|integer|Validates whether the input follows the Fibonacci integer sequence.| +|file|string|Validates whether file input is as a reqular filename.| +|hexRgbColor|string|Validates whether the input is a hex RGB color or not.| +|iban|string|Validates whether the input is a valid IBAN (International Bank Account Number) or not.| +|imei|string|Validates if the input is a valid IMEI.| +|ip|string|Validates whether the input is a valid IP address.| +|isbn|string|Validates whether the input is a valid ISBN or not.| +|json|string|Validates if the given input is a valid JSON.| +|xml|string|Validates if the given input is a valid XML.| +|languageCode|string|Validates whether the input is language code based on ISO 639.| +|luhn|string|Validate whether a given input is a Luhn number.| +|macAddress|string|Validates whether the input is a valid MAC address.| +|nfeAccessKey|string|Validates the access key of the Brazilian electronic invoice (NFe).| + +* Phone numbers should ALWAY be treated as a string since they MAY contain a leading zero. + +*Country specific formats* + +| Format | Type(s) | Description | +|--------|---------|-------------| +|bsn|string|Dutch social security number (BSN)| +|nip|string, integer|Polish VAT identification number (NIP)| +|nif|string, integer|Spanish fiscal identification number (NIF)| +|cnh|string, integer|Brazilian driver’s licence| +|cpf|string, integer| Validates a Brazillian CPF number | +|cnpj|string, integer|Validates if the input is a Brazilian National Reqistry of Legal Entities (CNPJ) number | + +* Dutch BSN numbers should ALWAY be treated as a string since they MAY contain a leading zero. + + +####Validations +Besides validations on type and string you can also use specific validations, these are contained in the validation array. Validation might be specific to certain types or formats e.g. minValue can only be applied values that can be turned into numeric value. And other validations might be of a more general nature e.g. required. + + + +| Validation | value | Description | +|--------|---------|-------------| +|between| |Validates whether the input is between two other values.| +|boolType| |Validates whether the type of the input is boolean.| +|boolVal| |Validates if the input results in a boolean value.| +|call| |Validates the return of a [callable][] for a given input.| +|callableType| |Validates whether the pseudo-type of the input is callable.| +|callback| |Validates the input using the return of a given callable.| +|charset| |Validates if a string is in a specific charset.| +|alwaysInvalid| | Validates any input as invalid| +|alwaysValid| | Validates any input as valid| +|anyOf| | This is a group validator that acts as an OR operator. AnyOf returns true if at least one inner validator passes.| +|arrayType| | Validates whether the type of an input is array| +|arrayVal| |Validates if the input is an array or if the input can be used as an array (instance of ArrayAcces or SimpleXMLElement.| +|attribute| | Validates an object attribute, even private ones.| +|consonant| | Validates if the input contains only consonants.| +|contains| | Validates if the input contains some value. | +|containsAny| | Validates if the input contains at least one of defined values.| +|control| | Validates if all of the characters in the provided string, are control characters. | +|countable| | Validates if the input is countable, in other words, if you’re allowed to use count() funtion on it. | +|decimal| | Validates whether the input matches the expected number or decimals.| +|each| | Validates whether each value in the input is valid according to another rule.| +|endsWith| | This validator is similar to Contains(), but validates only if the value is at the end of the input.| +|equals| | Validates if the input is equal to some value.| +|equivalent| | Validates if the input is equivalent to some value.| +|even| | Validates whether the input is an even number or not.| +|executable| |Validates if a file is an executable.| +|exists| | Validates files or directories.| +|extension| | Validates if the file extension matches the expected one. This rule is case-sensitive.| +|factor| | Validates if the input is a factor of the defined dividend.| +|falseVal| | Validates if a value is considered as false.| +|file| | Validates whether file input is as a reqular filename.| +|image| | Validates if the file is a valid image by checking its MIME type.| +|filterVar| | Validates the input with the PHP’s filter_var() function.| +|finite| | Validates if the input is a firtine number.| +|floatType| | Validates whether the type of the input is float.| +|floatVal| |Validate whether the input value is float.| +|graph| | Validates if all characters in the input are printable and actually creates visible output (no white space).| +|greaterThen| | Validates whether the input is greater than a value.| +|identical| | Validates if the input is identical to some value.| +|in| | Validates if the input is contained in a specific haystack. | +|infinite | | Validates if the input is an infinite number.| +|instance| | Validates if the input is an instance of the given class or interface.| +|iterableType| | Validates whether the pseudo-type of the input is iterable or not, in other words, if you're able to iterate over it with foreach language construct.| +|key| | Validates an array key.| +|keyNested| | Validates an array key or an object property using . to represent nested data.| +|keySet| | Validates a keys in a defined structure.| +|keyValue| | | +|leapDate| |Validates if a date is leap.| +|leapYear| |Validates if a year is leap.| +|length| | | +|lessThan| |Validates whether the input is less than a value.| +|lowercase | |Validates whether the characters in the input are lowercase.| +|not| | | +|notBlank| |Validates if the given input is not a blank value (null, zeros, empty strings or empty arrays, recursively).| +|notEmoji| |Validates if the input does not contain an emoji.| +|no| |Validates if value is considered as “No”.| +|noWhitespace| |Validates if a string contains no whitespace (spaces, tabs and line breaks).| +|noneOf| |Validates if NONE of the given validators validate.| +|max| |Validates whether the input is less than or equal to a value.| +|maxAge| |Validates a maximum age for a given date. The $format argument should be in accordance to PHP's date() function.| +|mimetype| |Validates if the input is a file and if its MIME type matches the expected one.| +|min| |Validates whether the input is greater than or equal to a value.| +|minAge| |Validates a minimum age for a given date. The $format argument should be in accordance to PHP's date() function.| +|multiple| |Validates if the input is a multiple of the given parameter.| +|negative| |Validates whether the input is a negative number.| + + +# Objects +An object is an data set conforming to schema, e.g for the schema pet we might have an object pluto. + +## Hydration +The process of transforming incoming data to objects is called hydration.