diff --git a/.gitignore b/.gitignore
new file mode 100755
index 000000000..162774c65
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+/vendor/
+/.idea/
+.DS_Store
+mock.json
+data-tests.php
+loader.php
+.phpunit.result.cache
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..6c5474450
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,32 @@
+dist: xenial
+
+arch:
+ - amd64
+
+os: linux
+
+language: shell
+
+services:
+- docker
+
+notifications:
+ email:
+ - team@appwrite.io
+
+before_script: docker run --rm --interactive --tty --volume "$(pwd)":/app composer update --ignore-platform-reqs --optimize-autoloader --no-plugins --no-scripts --prefer-dist
+
+before_install:
+- >
+ if [ ! -z "${DOCKERHUB_PULL_USERNAME:-}" ]; then
+ echo "${DOCKERHUB_PULL_PASSWORD}" | docker login --username "${DOCKERHUB_PULL_USERNAME}" --password-stdin
+ fi
+
+install:
+- docker-compose up -d
+- sleep 10
+
+script:
+- docker ps
+- docker-compose exec tests vendor/bin/phpunit --configuration phpunit.xml tests
+- docker-compose exec tests vendor/bin/psalm --show-info=true
diff --git a/Dockerfile b/Dockerfile
new file mode 100755
index 000000000..692ba1ac7
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,46 @@
+FROM composer:2.0 as step0
+
+ARG TESTING=false
+ENV TESTING=$TESTING
+
+WORKDIR /usr/local/src/
+
+COPY composer.lock /usr/local/src/
+COPY composer.json /usr/local/src/
+
+RUN composer update --ignore-platform-reqs --optimize-autoloader \
+ --no-plugins --no-scripts --prefer-dist
+
+FROM php:7.4-cli-alpine as final
+
+LABEL maintainer="team@appwrite.io"
+
+ENV PHP_SWOOLE_VERSION=v4.6.6
+
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+RUN \
+ apk update \
+ && apk add --no-cache postgresql-libs postgresql-dev make automake autoconf gcc g++ git brotli-dev \
+ && pecl install mongodb redis \
+ && docker-php-ext-enable mongodb redis \
+ && docker-php-ext-install opcache pgsql pdo_mysql pdo_pgsql \
+ ## Swoole Extension
+ && git clone --depth 1 --branch $PHP_SWOOLE_VERSION https://github.com/swoole/swoole-src.git \
+ && cd swoole-src \
+ && phpize \
+ && ./configure --enable-http2 \
+ && make && make install \
+ && cd .. \
+ && rm -rf /var/cache/apk/*
+
+WORKDIR /usr/src/code
+
+RUN echo extension=swoole.so >> /usr/local/etc/php/conf.d/swoole.ini
+
+COPY --from=step0 /usr/local/src/vendor /usr/src/code/vendor
+
+# Add Source Code
+COPY . /usr/src/code
+
+CMD [ "tail", "-f", "/dev/null" ]
diff --git a/README.md b/README.md
index 3be9984d0..e9e62c1e3 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,195 @@
-# database
+# Utopia Database
+
+[](https://travis-ci.com/utopia-php/database)
+
+[](https://appwrite.io/discord)
+
+Utopia framework database library is simple and lite library for managing application persistency using multiple database adapters. This library is aiming to be as simple and easy to learn and use. This library is maintained by the [Appwrite team](https://appwrite.io).
+
+Although this library is part of the [Utopia Framework](https://github.com/utopia-php/framework) project it is dependency free, and can be used as standalone with any other PHP project or framework.
+
+## Getting Started
+
+Install using composer:
+```bash
+composer require utopia-php/database
+```
+
+Initialization:
+
+```php
+ 'SET NAMES utf8mb4',
+ PDO::ATTR_TIMEOUT => 3, // Seconds
+ PDO::ATTR_PERSISTENT => true,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+]);
+
+$cache = new Cache(new NoCache()); // or use any cache adapter you wish
+
+$database = new Database(new MariaDB($pdo), $cache);
+$database->setNamespace('mydb');
+
+$database->create(); // Creates a new schema named `mydb`
+```
+
+**Creating a collection:**
+
+```php
+$database->createCollection('movies');
+
+// Add attributes
+$database->createAttribute('movies', 'name', Database::VAR_STRING, 128, true);
+$database->createAttribute('movies', 'director', Database::VAR_STRING, 128, true);
+$database->createAttribute('movies', 'year', Database::VAR_INTEGER, 0, true);
+$database->createAttribute('movies', 'price', Database::VAR_FLOAT, 0, true);
+$database->createAttribute('movies', 'active', Database::VAR_BOOLEAN, 0, true);
+$database->createAttribute('movies', 'generes', Database::VAR_STRING, 32, true, true, true);
+
+// Create an Index
+$database->createIndex('movies', 'index1', Database::INDEX_KEY, ['year'], [128], [Database::ORDER_ASC]);
+```
+
+**Create a document:**
+
+```php
+static::getDatabase()->createDocument('movies', new Document([
+ '$read' => ['*', 'user1', 'user2'],
+ '$write' => ['*', 'user1x', 'user2x'],
+ 'name' => 'Captain Marvel',
+ 'director' => 'Anna Boden & Ryan Fleck',
+ 'year' => 2019,
+ 'price' => 25.99,
+ 'active' => true,
+ 'generes' => ['science fiction', 'action', 'comics'],
+]));
+```
+
+**Find:**
+
+```php
+$documents = static::getDatabase()->find('movies', [
+ new Query('year', Query::TYPE_EQUAL, [2019]),
+]);
+```
+
+### Adapters
+
+Below is a list of supported adapters, and thier compatibly tested versions alongside a list of supported features and relevant limits.
+
+| Adapter | Status | Version |
+|---------|---------|---|
+| MariaDB | ✅ | 10.5 |
+| MySQL | ✅ | 8.0 |
+| Postgres | 🛠 | 13.0 |
+| MongoDB | 🛠 | 3.6 |
+| SQLlite | 🛠 | 3.35 |
+
+` ✅ - supported, 🛠 - work in progress`
+
+## TODOS
+
+- [ ] CRUD: Updated databases list method
+- [ ] CRUD: Validate original document before editing `$id`
+- [ ] CRUD: Test no one can overwrite exciting documents/collections without permission
+- [ ] FIND: Test for find timeout limits
+- [ ] FIND: Add a query validator (Limit queries to indexed attaributes only?)
+- [ ] FIND: Add support for more operators (search/match/like)
+- [ ] TEST: Missing Collection, DocumentId validators tests
+- [ ] TEST: Validate row size is not larger than allowed by adapter (MySQL/MariaDB ~16k)
+- [ ] TEST: Add test for creation of a unique index
+
+## Open Issues
+
+- Lazy index creation, maybe add a queue attribute to populate before creating the index?
+- In queries for arrays, should we create a dedicated index?
+
+## Limitations (to be completed per adapter)
+
+- ID max size can be 255 bytes
+- ID can only contain [^A-Za-z0-9]
+- Document max size is x bytes
+- Collection can have a max of x attributes
+- Collection can have a max of x indexes
+- Index value max size is x bytes. Values over x bytes are truncated
+
+## System Requirements
+
+Utopia Framework requires PHP 7.3 or later. We recommend using the latest PHP version whenever possible.
+
+## Tests
+
+To run all unit tests, use the following Docker command:
+
+```bash
+docker-compose exec tests vendor/bin/phpunit --configuration phpunit.xml tests
+```
+
+To run static code analysis, use the following Psalm command:
+
+```bash
+docker-compose exec tests vendor/bin/psalm --show-info=true
+```
+## Authors
+
+**Eldad Fux**
+
++ [https://twitter.com/eldadfux](https://twitter.com/eldadfux)
++ [https://github.com/eldadfux](https://github.com/eldadfux)
+
+**Brandon Leckemby**
+
++ [https://github.com/kodumbeats](https://github.com/kodumbeats)
++ [blog.leckemby.me](blog.leckemby.me)
+
+## Copyright and license
+
+The MIT License (MIT) [http://www.opensource.org/licenses/mit-license.php](http://www.opensource.org/licenses/mit-license.php)
diff --git a/SPEC.md b/SPEC.md
new file mode 100644
index 000000000..3c2422a52
--- /dev/null
+++ b/SPEC.md
@@ -0,0 +1,172 @@
+# Database Abstraction Layer
+
+The goal of this library is to serve as an abstraction layer on top of multiple database adapters and allow storage and query of JSON based documents and relationships.
+
+## Adapters
+
+This library will abstract multiple database technologies using the adapter's design patterns. Below is a list of various adapters we should consider to support:
+* MariaDB
+* MySQL
+* Postgres
+* MongoDB
+
+## Data Types
+
+This library will support storing and fetching of all common JSON simple and complex [data types](https://restfulapi.net/json-data-types/).
+
+### Simple Types
+
+* String
+* Integer
+* Float
+* Boolean
+* Null
+
+### Complex Types
+* Array
+* Object
+* Relationships
+ * Reference (collection / document)
+ * References (Array of - collection / document)
+
+Databases that don't support the storage of complex data types should store them as strings and parse them correctly when fetched.
+
+## Persistency
+
+Each database adapter should support the following action for fast storing and retrieval of collections of documents.
+
+**Databases** (Schemas for MariaDB)
+* create
+* delete
+
+**Collections** (Tables for MariaDB)
+* createCollection($name)
+* deleteCollection($name)
+
+**Attributes** (Table columns for MariaDB)
+* createAttribute(string $collection, string $name, string $type)
+* deleteAttribute(string $collection, string $name)
+
+**Indices** (Table indices for MariaDB)
+* createIndex(string $collection, string $name, string $type)
+* deleteIndex(string $collection, string $name, string $type)
+
+**Documents** (Table rows columns for MariaDB)
+* getDocument(string $collection, $id)
+* createDocument(string $collection, array $data)
+* updateDocument(string $collection, $id, array $data)
+* deleteDocument(string $collection, $id)
+
+## Queries
+
+Each database adapter should allow querying simple and advanced queries in consideration of underline limitations.
+
+Method for quering data:
+* find(string $collection, $filters)
+* findFirst(string $collection, $filters)
+* findLast(string $collection, $filters)
+* count(string $collection, $filters)
+
+### Supported Query Operations
+* Equal (==)
+* Not Equal (!=)
+* Less Than (<)
+* Less or equal (<=)
+* Bigger Than (>)
+* Bigger or equal (>=)
+* Containes / In
+* Is Null
+* Is Empty
+
+### Joins / Relationships
+
+## Paging
+
+Each database adapter should support two methods for paging. The first method is the classic `Limit and Offset`. The second method is `After` paging, which allows better performance at a larger scale.
+
+## Orders
+
+Multi-column support, order type, use native types
+
+## Features
+
+> Each database collection should hold parallel tables (if needed) with row-level metadata, used to abstract features that are not enabled in all the adapters.
+
+### Row-level Security
+
+### GEO Queries
+
+### Free Search
+
+### Filters
+
+Allow to apply custom filters on specific pre-chosen fields. Avaliable filters:
+
+* Encryption
+* JSON (might be redundent with object support)
+* Hashing (md5,bcrypt)
+
+## Caching
+
+The library should support memory caching using internal or external memory devices for all read operations. Write operations should actively clean or update the cache.
+
+## Encoding
+
+All database adapters should support UTF-8 encoding and accept emoji characters.
+
+## Documentation
+
+* Data Types
+* Operations
+* Security
+* Benchmarks
+* Performance Tips
+* Known Limitations
+
+## Tests
+
+* Check for SQL Injections
+
+## Examples (MariaDB)
+
+**Collections Metadata**
+
+```sql
+CREATE TABLE IF NOT EXISTS `collections` (
+ `_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `_metadata` text() DEFAULT NULL,
+ PRIMARY KEY (`id`),
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+**Documents**
+
+```sql
+CREATE TABLE IF NOT EXISTS `documents_[NAME]` (
+ `_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `_uid` varchar(128) NOT NULL AUTO_INCREMENT,
+ `custom1` text() DEFAULT NULL,
+ `custom2` text() DEFAULT NULL,
+ `custom3` text() DEFAULT NULL,
+ PRIMARY KEY (`_id`),
+ UNIQUE KEY `_index1` (`$id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+**Documents Authorization**
+
+```sql
+CREATE TABLE IF NOT EXISTS `documents_[NAME]_authorization` (
+ `_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `_document` varchar(128) DEFAULT NULL,
+ `_role` varchar(128) DEFAULT NULL,
+ `_action` varchar(128) DEFAULT NULL,
+ PRIMARY KEY (`_id`),
+ KEY `_index1` (`_document`)
+ KEY `_index1` (`_document`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+## Optimization Tools
+
+https://www.eversql.com/
\ No newline at end of file
diff --git a/architecture.drawio.svg b/architecture.drawio.svg
new file mode 100644
index 000000000..85dfc931a
--- /dev/null
+++ b/architecture.drawio.svg
@@ -0,0 +1,68 @@
+
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100755
index 000000000..779cde85c
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,37 @@
+{
+ "name": "utopia-php/database",
+ "description": "A simple library to manage application persistency using multiple database adapters",
+ "type": "library",
+ "keywords": ["php","framework", "upf", "utopia", "database"],
+ "license": "MIT",
+ "minimum-stability": "stable",
+ "authors": [
+ {
+ "name": "Eldad Fux",
+ "email": "eldad@appwrite.io"
+ },
+ {
+ "name": "Brandon Leckemby",
+ "email": "brandon@appwrite.io"
+ }
+ ],
+ "autoload": {
+ "psr-4": {"Utopia\\Database\\": "src/Database"}
+ },
+ "autoload-dev": {
+ "psr-4": {"Utopia\\Tests\\": "tests/Database"}
+ },
+ "require": {
+ "php": ">=7.1",
+ "ext-pdo": "*",
+ "ext-redis": "*",
+ "ext-mongodb": "*",
+ "utopia-php/framework": "0.*.*",
+ "utopia-php/cache": "0.4.*",
+ "mongodb/mongodb": "1.8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.4",
+ "vimeo/psalm": "4.0.1"
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 000000000..9dc980eac
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,3931 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "dd2546d5cd02cf55391c4a5cd4768bd0",
+ "packages": [
+ {
+ "name": "composer/package-versions-deprecated",
+ "version": "1.11.99.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/package-versions-deprecated.git",
+ "reference": "7413f0b55a051e89485c5cb9f765fe24bb02a7b6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/7413f0b55a051e89485c5cb9f765fe24bb02a7b6",
+ "reference": "7413f0b55a051e89485c5cb9f765fe24bb02a7b6",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.1.0 || ^2.0",
+ "php": "^7 || ^8"
+ },
+ "replace": {
+ "ocramius/package-versions": "1.11.99"
+ },
+ "require-dev": {
+ "composer/composer": "^1.9.3 || ^2.0@dev",
+ "ext-zip": "^1.13",
+ "phpunit/phpunit": "^6.5 || ^7"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "PackageVersions\\Installer",
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PackageVersions\\": "src/PackageVersions"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be"
+ }
+ ],
+ "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
+ "support": {
+ "issues": "https://github.com/composer/package-versions-deprecated/issues",
+ "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.1"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-11-11T10:22:58+00:00"
+ },
+ {
+ "name": "jean85/pretty-package-versions",
+ "version": "1.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Jean85/pretty-package-versions.git",
+ "reference": "1e0104b46f045868f11942aea058cd7186d6c303"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/1e0104b46f045868f11942aea058cd7186d6c303",
+ "reference": "1e0104b46f045868f11942aea058cd7186d6c303",
+ "shasum": ""
+ },
+ "require": {
+ "composer/package-versions-deprecated": "^1.8.0",
+ "php": "^7.0|^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.0|^8.5|^9.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Jean85\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Alessandro Lai",
+ "email": "alessandro.lai85@gmail.com"
+ }
+ ],
+ "description": "A wrapper for ocramius/package-versions to get pretty versions strings",
+ "keywords": [
+ "composer",
+ "package",
+ "release",
+ "versions"
+ ],
+ "support": {
+ "issues": "https://github.com/Jean85/pretty-package-versions/issues",
+ "source": "https://github.com/Jean85/pretty-package-versions/tree/1.6.0"
+ },
+ "time": "2021-02-04T16:20:16+00:00"
+ },
+ {
+ "name": "mongodb/mongodb",
+ "version": "1.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mongodb/mongo-php-library.git",
+ "reference": "953dbc19443aa9314c44b7217a16873347e6840d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/953dbc19443aa9314c44b7217a16873347e6840d",
+ "reference": "953dbc19443aa9314c44b7217a16873347e6840d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-hash": "*",
+ "ext-json": "*",
+ "ext-mongodb": "^1.8.1",
+ "jean85/pretty-package-versions": "^1.2",
+ "php": "^7.0 || ^8.0",
+ "symfony/polyfill-php80": "^1.19"
+ },
+ "require-dev": {
+ "squizlabs/php_codesniffer": "^3.5, <3.5.5",
+ "symfony/phpunit-bridge": "5.x-dev"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "MongoDB\\": "src/"
+ },
+ "files": [
+ "src/functions.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Andreas Braun",
+ "email": "andreas.braun@mongodb.com"
+ },
+ {
+ "name": "Jeremy Mikola",
+ "email": "jmikola@gmail.com"
+ }
+ ],
+ "description": "MongoDB driver library",
+ "homepage": "https://jira.mongodb.org/browse/PHPLIB",
+ "keywords": [
+ "database",
+ "driver",
+ "mongodb",
+ "persistence"
+ ],
+ "support": {
+ "issues": "https://github.com/mongodb/mongo-php-library/issues",
+ "source": "https://github.com/mongodb/mongo-php-library/tree/1.8.0"
+ },
+ "time": "2020-11-25T12:26:02+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.22.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91",
+ "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.22-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ],
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-01-07T16:49:33+00:00"
+ },
+ {
+ "name": "utopia-php/cache",
+ "version": "0.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/utopia-php/cache.git",
+ "reference": "8c48eff73219c8c1ac2807909f0a38f3480c8938"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/utopia-php/cache/zipball/8c48eff73219c8c1ac2807909f0a38f3480c8938",
+ "reference": "8c48eff73219c8c1ac2807909f0a38f3480c8938",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-redis": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "vimeo/psalm": "4.0.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Utopia\\Cache\\": "src/Cache"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Eldad Fux",
+ "email": "eldad@appwrite.io"
+ }
+ ],
+ "description": "A simple cache library to manage application cache storing, loading and purging",
+ "keywords": [
+ "cache",
+ "framework",
+ "php",
+ "upf",
+ "utopia"
+ ],
+ "support": {
+ "issues": "https://github.com/utopia-php/cache/issues",
+ "source": "https://github.com/utopia-php/cache/tree/0.4.1"
+ },
+ "time": "2021-04-29T18:41:43+00:00"
+ },
+ {
+ "name": "utopia-php/framework",
+ "version": "0.14.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/utopia-php/framework.git",
+ "reference": "92d4a36f3b0e22393a31877c5317c96e01760339"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/utopia-php/framework/zipball/92d4a36f3b0e22393a31877c5317c96e01760339",
+ "reference": "92d4a36f3b0e22393a31877c5317c96e01760339",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.4",
+ "vimeo/psalm": "4.0.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Utopia\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Eldad Fux",
+ "email": "eldad@appwrite.io"
+ }
+ ],
+ "description": "A simple, light and advanced PHP framework",
+ "keywords": [
+ "framework",
+ "php",
+ "upf"
+ ],
+ "support": {
+ "issues": "https://github.com/utopia-php/framework/issues",
+ "source": "https://github.com/utopia-php/framework/tree/0.14.0"
+ },
+ "time": "2021-04-15T21:01:44+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "amphp/amp",
+ "version": "v2.5.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/amp.git",
+ "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/amp/zipball/efca2b32a7580087adb8aabbff6be1dc1bb924a9",
+ "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "dev-master",
+ "amphp/phpunit-util": "^1",
+ "ext-json": "*",
+ "jetbrains/phpstorm-stubs": "^2019.3",
+ "phpunit/phpunit": "^6.0.9 | ^7",
+ "psalm/phar": "^3.11@dev",
+ "react/promise": "^2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Amp\\": "lib"
+ },
+ "files": [
+ "lib/functions.php",
+ "lib/Internal/functions.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Daniel Lowrey",
+ "email": "rdlowrey@php.net"
+ },
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Bob Weinand",
+ "email": "bobwei9@hotmail.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "A non-blocking concurrency framework for PHP applications.",
+ "homepage": "http://amphp.org/amp",
+ "keywords": [
+ "async",
+ "asynchronous",
+ "awaitable",
+ "concurrency",
+ "event",
+ "event-loop",
+ "future",
+ "non-blocking",
+ "promise"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/amphp",
+ "issues": "https://github.com/amphp/amp/issues",
+ "source": "https://github.com/amphp/amp/tree/v2.5.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2021-01-10T17:06:37+00:00"
+ },
+ {
+ "name": "amphp/byte-stream",
+ "version": "v1.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/byte-stream.git",
+ "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd",
+ "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^2",
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "dev-master",
+ "amphp/phpunit-util": "^1.4",
+ "friendsofphp/php-cs-fixer": "^2.3",
+ "jetbrains/phpstorm-stubs": "^2019.3",
+ "phpunit/phpunit": "^6 || ^7 || ^8",
+ "psalm/phar": "^3.11.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Amp\\ByteStream\\": "lib"
+ },
+ "files": [
+ "lib/functions.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "A stream abstraction to make working with non-blocking I/O simple.",
+ "homepage": "http://amphp.org/byte-stream",
+ "keywords": [
+ "amp",
+ "amphp",
+ "async",
+ "io",
+ "non-blocking",
+ "stream"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/amphp",
+ "issues": "https://github.com/amphp/byte-stream/issues",
+ "source": "https://github.com/amphp/byte-stream/tree/v1.8.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2021-03-30T17:13:30+00:00"
+ },
+ {
+ "name": "composer/semver",
+ "version": "3.2.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/a02fdf930a3c1c3ed3a49b5f63859c0c20e10464",
+ "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^0.12.54",
+ "symfony/phpunit-bridge": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Semver\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/semver/issues",
+ "source": "https://github.com/composer/semver/tree/3.2.4"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-11-13T08:59:24+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "1.4.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "f27e06cd9675801df441b3656569b328e04aa37c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f27e06cd9675801df441b3656569b328e04aa37c",
+ "reference": "f27e06cd9675801df441b3656569b328e04aa37c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0",
+ "psr/log": "^1.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^0.12.55",
+ "symfony/phpunit-bridge": "^4.2 || ^5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without Xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/xdebug-handler/issues",
+ "source": "https://github.com/composer/xdebug-handler/tree/1.4.6"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-03-25T17:01:18+00:00"
+ },
+ {
+ "name": "dnoegel/php-xdg-base-dir",
+ "version": "v0.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dnoegel/php-xdg-base-dir.git",
+ "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
+ "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "XdgBaseDir\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "implementation of xdg base directory specification for php",
+ "support": {
+ "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues",
+ "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1"
+ },
+ "time": "2019-12-04T15:06:13+00:00"
+ },
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b",
+ "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^8.0",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.13 || 1.0.0-alpha2",
+ "phpstan/phpstan": "^0.12",
+ "phpstan/phpstan-phpunit": "^0.12",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/1.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-11-10T18:47:58+00:00"
+ },
+ {
+ "name": "felixfbecker/advanced-json-rpc",
+ "version": "v3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git",
+ "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/06f0b06043c7438959dbdeed8bb3f699a19be22e",
+ "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e",
+ "shasum": ""
+ },
+ "require": {
+ "netresearch/jsonmapper": "^1.0 || ^2.0",
+ "php": "^7.1 || ^8.0",
+ "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.0 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "AdvancedJsonRpc\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "ISC"
+ ],
+ "authors": [
+ {
+ "name": "Felix Becker",
+ "email": "felix.b@outlook.com"
+ }
+ ],
+ "description": "A more advanced JSONRPC implementation",
+ "support": {
+ "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues",
+ "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.0"
+ },
+ "time": "2021-01-10T17:48:47+00:00"
+ },
+ {
+ "name": "felixfbecker/language-server-protocol",
+ "version": "1.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/felixfbecker/php-language-server-protocol.git",
+ "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
+ "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "*",
+ "squizlabs/php_codesniffer": "^3.1",
+ "vimeo/psalm": "^4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "LanguageServerProtocol\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "ISC"
+ ],
+ "authors": [
+ {
+ "name": "Felix Becker",
+ "email": "felix.b@outlook.com"
+ }
+ ],
+ "description": "PHP classes for the Language Server Protocol",
+ "keywords": [
+ "language",
+ "microsoft",
+ "php",
+ "server"
+ ],
+ "support": {
+ "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues",
+ "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1"
+ },
+ "time": "2021-02-22T14:02:09+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.10.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220",
+ "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "replace": {
+ "myclabs/deep-copy": "self.version"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.0",
+ "doctrine/common": "^2.6",
+ "phpunit/phpunit": "^7.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ },
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-11-13T09:40:50+00:00"
+ },
+ {
+ "name": "netresearch/jsonmapper",
+ "version": "v2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cweiske/jsonmapper.git",
+ "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/e0f1e33a71587aca81be5cffbb9746510e1fe04e",
+ "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-pcre": "*",
+ "ext-reflection": "*",
+ "ext-spl": "*",
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4 || ~7.0",
+ "squizlabs/php_codesniffer": "~3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "JsonMapper": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "OSL-3.0"
+ ],
+ "authors": [
+ {
+ "name": "Christian Weiske",
+ "email": "cweiske@cweiske.de",
+ "homepage": "http://github.com/cweiske/jsonmapper/",
+ "role": "Developer"
+ }
+ ],
+ "description": "Map nested JSON structures onto PHP classes",
+ "support": {
+ "email": "cweiske@cweiske.de",
+ "issues": "https://github.com/cweiske/jsonmapper/issues",
+ "source": "https://github.com/cweiske/jsonmapper/tree/master"
+ },
+ "time": "2020-04-16T18:48:43+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v4.10.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c6d052fc58cb876152f89f532b95a8d7907e7f0e",
+ "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.4"
+ },
+ "time": "2020-12-20T10:01:03+00:00"
+ },
+ {
+ "name": "openlss/lib-array2xml",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nullivex/lib-array2xml.git",
+ "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nullivex/lib-array2xml/zipball/a91f18a8dfc69ffabe5f9b068bc39bb202c81d90",
+ "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "LSS": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Bryan Tong",
+ "email": "bryan@nullivex.com",
+ "homepage": "https://www.nullivex.com"
+ },
+ {
+ "name": "Tony Butler",
+ "email": "spudz76@gmail.com",
+ "homepage": "https://www.nullivex.com"
+ }
+ ],
+ "description": "Array2XML conversion library credit to lalit.org",
+ "homepage": "https://www.nullivex.com",
+ "keywords": [
+ "array",
+ "array conversion",
+ "xml",
+ "xml conversion"
+ ],
+ "support": {
+ "issues": "https://github.com/nullivex/lib-array2xml/issues",
+ "source": "https://github.com/nullivex/lib-array2xml/tree/master"
+ },
+ "time": "2019-03-29T20:06:56+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
+ "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/master"
+ },
+ "time": "2020-06-27T14:33:11+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "bae7c545bef187884426f042434e561ab1ddb182"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182",
+ "reference": "bae7c545bef187884426f042434e561ab1ddb182",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.1.0"
+ },
+ "time": "2021-02-23T14:00:09+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-common",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-2.x": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+ "homepage": "http://www.phpdoc.org",
+ "keywords": [
+ "FQSEN",
+ "phpDocumentor",
+ "phpdoc",
+ "reflection",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
+ },
+ "time": "2020-06-27T09:03:43+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-docblock",
+ "version": "5.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+ "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556",
+ "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556",
+ "shasum": ""
+ },
+ "require": {
+ "ext-filter": "*",
+ "php": "^7.2 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.2",
+ "phpdocumentor/type-resolver": "^1.3",
+ "webmozart/assert": "^1.9.1"
+ },
+ "require-dev": {
+ "mockery/mockery": "~1.3.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ },
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "account@ijaap.nl"
+ }
+ ],
+ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master"
+ },
+ "time": "2020-09-03T19:13:55+00:00"
+ },
+ {
+ "name": "phpdocumentor/type-resolver",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/TypeResolver.git",
+ "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+ "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.0"
+ },
+ "require-dev": {
+ "ext-tokenizer": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-1.x": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+ "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0"
+ },
+ "time": "2020-09-17T18:55:26+00:00"
+ },
+ {
+ "name": "phpspec/prophecy",
+ "version": "1.13.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpspec/prophecy.git",
+ "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea",
+ "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.2",
+ "php": "^7.2 || ~8.0, <8.1",
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "sebastian/comparator": "^3.0 || ^4.0",
+ "sebastian/recursion-context": "^3.0 || ^4.0"
+ },
+ "require-dev": {
+ "phpspec/phpspec": "^6.0",
+ "phpunit/phpunit": "^8.0 || ^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.11.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Prophecy\\": "src/Prophecy"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ },
+ {
+ "name": "Marcello Duarte",
+ "email": "marcello.duarte@gmail.com"
+ }
+ ],
+ "description": "Highly opinionated mocking framework for PHP 5.3+",
+ "homepage": "https://github.com/phpspec/prophecy",
+ "keywords": [
+ "Double",
+ "Dummy",
+ "fake",
+ "mock",
+ "spy",
+ "stub"
+ ],
+ "support": {
+ "issues": "https://github.com/phpspec/prophecy/issues",
+ "source": "https://github.com/phpspec/prophecy/tree/1.13.0"
+ },
+ "time": "2021-03-17T13:42:18+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "f6293e1b30a2354e8428e004689671b83871edde"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde",
+ "reference": "f6293e1b30a2354e8428e004689671b83871edde",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.10.2",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.3",
+ "phpunit/php-text-template": "^2.0.2",
+ "sebastian/code-unit-reverse-lookup": "^2.0.2",
+ "sebastian/complexity": "^2.0",
+ "sebastian/environment": "^5.1.2",
+ "sebastian/lines-of-code": "^1.0.3",
+ "sebastian/version": "^3.0.1",
+ "theseer/tokenizer": "^1.2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcov": "*",
+ "ext-xdebug": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-03-28T07:26:59+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8",
+ "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:57:25+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.5.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "c73c6737305e779771147af66c96ca6a7ed8a741"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c73c6737305e779771147af66c96ca6a7ed8a741",
+ "reference": "c73c6737305e779771147af66c96ca6a7ed8a741",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.3.1",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.10.1",
+ "phar-io/manifest": "^2.0.1",
+ "phar-io/version": "^3.0.2",
+ "php": ">=7.3",
+ "phpspec/prophecy": "^1.12.1",
+ "phpunit/php-code-coverage": "^9.2.3",
+ "phpunit/php-file-iterator": "^3.0.5",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.3",
+ "phpunit/php-timer": "^5.0.2",
+ "sebastian/cli-parser": "^1.0.1",
+ "sebastian/code-unit": "^1.0.6",
+ "sebastian/comparator": "^4.0.5",
+ "sebastian/diff": "^4.0.3",
+ "sebastian/environment": "^5.1.3",
+ "sebastian/exporter": "^4.0.3",
+ "sebastian/global-state": "^5.0.1",
+ "sebastian/object-enumerator": "^4.0.3",
+ "sebastian/resource-operations": "^3.0.3",
+ "sebastian/type": "^2.3",
+ "sebastian/version": "^3.0.2"
+ },
+ "require-dev": {
+ "ext-pdo": "*",
+ "phpspec/prophecy-phpunit": "^2.0.1"
+ },
+ "suggest": {
+ "ext-soap": "*",
+ "ext-xdebug": "*"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.5-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ],
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.4"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/donate.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-03-23T07:16:29+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
+ "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/1.1.1"
+ },
+ "time": "2021-03-05T17:36:06+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "1.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
+ "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.3"
+ },
+ "time": "2020-03-23T09:12:05+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:08:49+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "55f4261989e546dc112258c7a75935a81a7ce382"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382",
+ "reference": "55f4261989e546dc112258c7a75935a81a7ce382",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:49:45+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.7",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:52:27+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d",
+ "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:10:38+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "388b6ced16caa751030f6a69e588299fa09200ac"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac",
+ "reference": "388b6ced16caa751030f6a69e588299fa09200ac",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:52:38+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65",
+ "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "http://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:24:23+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "a90ccbddffa067b51f574dea6eb25d5680839455"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455",
+ "reference": "a90ccbddffa067b51f574dea6eb25d5680839455",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:55:19+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc",
+ "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.6",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-28T06:42:11+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172",
+ "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:17:30+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:45:17+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "2.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2",
+ "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/2.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:18:59+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v5.2.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d",
+ "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php73": "^1.8",
+ "symfony/polyfill-php80": "^1.15",
+ "symfony/service-contracts": "^1.1|^2",
+ "symfony/string": "^5.1"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<4.4",
+ "symfony/dotenv": "<5.1",
+ "symfony/event-dispatcher": "<4.4",
+ "symfony/lock": "<4.4",
+ "symfony/process": "<4.4"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0"
+ },
+ "require-dev": {
+ "psr/log": "~1.0",
+ "symfony/config": "^4.4|^5.0",
+ "symfony/dependency-injection": "^4.4|^5.0",
+ "symfony/event-dispatcher": "^4.4|^5.0",
+ "symfony/lock": "^4.4|^5.0",
+ "symfony/process": "^4.4|^5.0",
+ "symfony/var-dumper": "^4.4|^5.0"
+ },
+ "suggest": {
+ "psr/log": "For using the console logger",
+ "symfony/event-dispatcher": "",
+ "symfony/lock": "",
+ "symfony/process": ""
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases the creation of beautiful and testable command line interfaces",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "cli",
+ "command line",
+ "console",
+ "terminal"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/console/tree/v5.2.6"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-03-28T09:42:18+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.22.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "c6c942b1ac76c82448322025e084cadc56048b4e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e",
+ "reference": "c6c942b1ac76c82448322025e084cadc56048b4e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.22-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-01-07T16:49:33+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "v1.22.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5601e09b69f26c1828b13b6bb87cb07cddba3170",
+ "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.22-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-01-22T09:19:47+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-normalizer",
+ "version": "v1.22.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+ "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248",
+ "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.22-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ],
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's Normalizer class and related functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "intl",
+ "normalizer",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-01-22T09:19:47+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.22.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "5232de97ee3b75b0360528dae24e73db49566ab1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1",
+ "reference": "5232de97ee3b75b0360528dae24e73db49566ab1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.22-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-01-22T09:19:47+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php73",
+ "version": "v1.22.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php73.git",
+ "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2",
+ "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.22-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php73\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ],
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-01-07T16:49:33+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v2.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
+ "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "psr/container": "^1.1"
+ },
+ "suggest": {
+ "symfony/service-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.4-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v2.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-04-01T10:43:52+00:00"
+ },
+ {
+ "name": "symfony/string",
+ "version": "v5.2.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572",
+ "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-normalizer": "~1.0",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php80": "~1.15"
+ },
+ "require-dev": {
+ "symfony/error-handler": "^4.4|^5.0",
+ "symfony/http-client": "^4.4|^5.0",
+ "symfony/translation-contracts": "^1.1|^2",
+ "symfony/var-exporter": "^4.4|^5.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "files": [
+ "Resources/functions.php"
+ ],
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/string/tree/v5.2.6"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-03-17T17:12:15+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "75a63c33a8577608444246075ea0af0d052e452a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a",
+ "reference": "75a63c33a8577608444246075ea0af0d052e452a",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/master"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2020-07-12T23:59:07+00:00"
+ },
+ {
+ "name": "vimeo/psalm",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/vimeo/psalm.git",
+ "reference": "b1e2e30026936ef8d5bf6a354d1c3959b6231f44"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/vimeo/psalm/zipball/b1e2e30026936ef8d5bf6a354d1c3959b6231f44",
+ "reference": "b1e2e30026936ef8d5bf6a354d1c3959b6231f44",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^2.1",
+ "amphp/byte-stream": "^1.5",
+ "composer/package-versions-deprecated": "^1.8.0",
+ "composer/semver": "^1.4 || ^2.0 || ^3.0",
+ "composer/xdebug-handler": "^1.1",
+ "dnoegel/php-xdg-base-dir": "^0.1.1",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "felixfbecker/advanced-json-rpc": "^3.0.3",
+ "felixfbecker/language-server-protocol": "^1.4",
+ "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0",
+ "nikic/php-parser": "^4.10.1",
+ "openlss/lib-array2xml": "^1.0",
+ "php": "^7.3|^8",
+ "sebastian/diff": "^3.0 || ^4.0",
+ "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0",
+ "webmozart/glob": "^4.1",
+ "webmozart/path-util": "^2.3"
+ },
+ "provide": {
+ "psalm/psalm": "self.version"
+ },
+ "require-dev": {
+ "amphp/amp": "^2.4.2",
+ "bamarni/composer-bin-plugin": "^1.2",
+ "brianium/paratest": "^4.0.0",
+ "ext-curl": "*",
+ "phpdocumentor/reflection-docblock": "^5",
+ "phpmyadmin/sql-parser": "5.1.0",
+ "phpspec/prophecy": ">=1.9.0",
+ "phpunit/phpunit": "^9.0",
+ "psalm/plugin-phpunit": "^0.13",
+ "slevomat/coding-standard": "^5.0",
+ "squizlabs/php_codesniffer": "^3.5",
+ "symfony/process": "^4.3",
+ "weirdan/prophecy-shim": "^1.0 || ^2.0"
+ },
+ "suggest": {
+ "ext-igbinary": "^2.0.5"
+ },
+ "bin": [
+ "psalm",
+ "psalm-language-server",
+ "psalm-plugin",
+ "psalm-refactor",
+ "psalter"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.x-dev",
+ "dev-3.x": "3.x-dev",
+ "dev-2.x": "2.x-dev",
+ "dev-1.x": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psalm\\": "src/Psalm/"
+ },
+ "files": [
+ "src/functions.php",
+ "src/spl_object_id.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Matthew Brown"
+ }
+ ],
+ "description": "A static analysis tool for finding errors in PHP applications",
+ "keywords": [
+ "code",
+ "inspection",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/vimeo/psalm/issues",
+ "source": "https://github.com/vimeo/psalm/tree/4.0.1"
+ },
+ "time": "2020-10-20T13:40:17+00:00"
+ },
+ {
+ "name": "webmozart/assert",
+ "version": "1.9.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozarts/assert.git",
+ "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
+ "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.3 || ^7.0 || ^8.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<0.12.20",
+ "vimeo/psalm": "<3.9.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.36 || ^7.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
+ "support": {
+ "issues": "https://github.com/webmozarts/assert/issues",
+ "source": "https://github.com/webmozarts/assert/tree/1.9.1"
+ },
+ "time": "2020-07-08T17:02:28+00:00"
+ },
+ {
+ "name": "webmozart/glob",
+ "version": "4.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozarts/glob.git",
+ "reference": "06358fafde0f32edb4513f4fd88fe113a40c90ee"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozarts/glob/zipball/06358fafde0f32edb4513f4fd88fe113a40c90ee",
+ "reference": "06358fafde0f32edb4513f4fd88fe113a40c90ee",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.3 || ^8.0.0",
+ "webmozart/path-util": "^2.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.0",
+ "symfony/filesystem": "^5.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Glob\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "A PHP implementation of Ant's glob.",
+ "support": {
+ "issues": "https://github.com/webmozarts/glob/issues",
+ "source": "https://github.com/webmozarts/glob/tree/4.3.0"
+ },
+ "time": "2021-01-21T06:17:15+00:00"
+ },
+ {
+ "name": "webmozart/path-util",
+ "version": "2.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozart/path-util.git",
+ "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozart/path-util/zipball/d939f7edc24c9a1bb9c0dee5cb05d8e859490725",
+ "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "webmozart/assert": "~1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.6",
+ "sebastian/version": "^1.0.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\PathUtil\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.",
+ "support": {
+ "issues": "https://github.com/webmozart/path-util/issues",
+ "source": "https://github.com/webmozart/path-util/tree/2.3.0"
+ },
+ "time": "2015-12-17T08:42:14+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=7.1",
+ "ext-pdo": "*",
+ "ext-redis": "*",
+ "ext-mongodb": "*"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "2.0.0"
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..78a08479b
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,79 @@
+version: '3.1'
+
+services:
+
+ tests:
+ container_name: tests
+ build:
+ context: .
+ networks:
+ - database
+ volumes:
+ - ./:/usr/src/code
+
+ postgres:
+ image: postgres:13
+ container_name: utopia-postgres
+ networks:
+ - database
+ ports:
+ - "8700:5432"
+ environment:
+ POSTGRES_USER: root
+ POSTGRES_PASSWORD: password
+
+ cockroach:
+ image: cockroachdb/cockroach:v20.2.0
+ container_name: utopia-cockroach
+ command: start-single-node --insecure --logtostderr
+ networks:
+ - database
+ ports:
+ - "8704:26257"
+ - "8705:8080"
+
+ mariadb:
+ image: mariadb:10.5
+ container_name: utopia-mariadb
+ networks:
+ - database
+ ports:
+ - "8701:3306"
+ environment:
+ - MYSQL_ROOT_PASSWORD=password
+
+ mongo:
+ image: mongo:3.6
+ container_name: utopia-mongo
+ networks:
+ - database
+ ports:
+ - "8702:27017"
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: root
+ MONGO_INITDB_ROOT_PASSWORD: example
+
+ mysql:
+ image: mysql:8.0
+ container_name: utopia-mysql
+ networks:
+ - database
+ ports:
+ - "8703:3307"
+ environment:
+ MYSQL_ROOT_PASSWORD: password
+ MYSQL_DATABASE: default
+ MYSQL_USER: user
+ MYSQL_PASSWORD: password
+ MYSQL_TCP_PORT: 3307
+ cap_add:
+ - SYS_NICE
+
+ redis:
+ image: redis:6.0-alpine
+ container_name: utopia-redis
+ networks:
+ - database
+
+networks:
+ database:
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100755
index 000000000..ec39a7ede
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,16 @@
+
+
+
+ ./tests/
+
+
+
\ No newline at end of file
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 000000000..7c0333df3
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php
new file mode 100644
index 000000000..121c09ae8
--- /dev/null
+++ b/src/Database/Adapter.php
@@ -0,0 +1,301 @@
+debug[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getDebug(): array
+ {
+ return $this->debug;
+ }
+
+ /**
+ * return $this
+ */
+ public function resetDebug(): self
+ {
+ $this->debug = [];
+
+ return $this;
+ }
+
+ /**
+ * Set Namespace.
+ *
+ * Set namespace to divide different scope of data sets
+ *
+ * @param $namespace
+ *
+ * @throws Exception
+ *
+ * @return bool
+ */
+ public function setNamespace(string $namespace): bool
+ {
+ if (empty($namespace)) {
+ throw new Exception('Missing namespace');
+ }
+
+ $this->namespace = $namespace;
+
+ return true;
+ }
+
+ /**
+ * Get Namespace.
+ *
+ * Get namespace of current set scope
+ *
+ * @throws Exception
+ *
+ * @return string
+ */
+ public function getNamespace(): string
+ {
+ if (empty($this->namespace)) {
+ throw new Exception('Missing namespace');
+ }
+
+ return $this->namespace;
+ }
+
+ /**
+ * Create Database
+ *
+ * @return bool
+ */
+ abstract public function create(): bool;
+
+ /**
+ * Check if database exists
+ *
+ * @return bool
+ */
+ abstract public function exists(): bool;
+
+ /**
+ * List Databases
+ *
+ * @return array
+ */
+ abstract public function list(): array;
+
+ /**
+ * Delete Database
+ *
+ * @return bool
+ */
+ abstract public function delete(): bool;
+
+ /**
+ * Create Collection
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ abstract public function createCollection(string $name): bool;
+
+ /**
+ * Delete Collection
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ abstract public function deleteCollection(string $name): bool;
+
+ /**
+ * Create Attribute
+ *
+ * @param string $collection
+ * @param string $id
+ * @param string $type
+ * @param int $size
+ * @param bool $array
+ *
+ * @return bool
+ */
+ abstract public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false): bool;
+
+ /**
+ * Delete Attribute
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return bool
+ */
+ abstract public function deleteAttribute(string $collection, string $id): bool;
+
+ /**
+ * Create Index
+ *
+ * @param string $collection
+ * @param string $id
+ * @param string $type
+ * @param array $attributes
+ * @param array $lengths
+ * @param array $orders
+ *
+ * @return bool
+ */
+ abstract public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool;
+
+ /**
+ * Delete Index
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return bool
+ */
+ abstract public function deleteIndex(string $collection, string $id): bool;
+
+ /**
+ * Get Document
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return Document
+ */
+ abstract public function getDocument(string $collection, string $id): Document;
+
+ /**
+ * Create Document
+ *
+ * @param string $collection
+ * @param Document $document
+ *
+ * @return Document
+ */
+ abstract public function createDocument(string $collection, Document $document): Document;
+
+ /**
+ * Update Document
+ *
+ * @param string $collection
+ * @param Document $document
+ *
+ * @return Document
+ */
+ abstract public function updateDocument(string $collection, Document $document): Document;
+
+ /**
+ * Delete Document
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return bool
+ */
+ abstract public function deleteDocument(string $collection, string $id): bool;
+
+ /**
+ * Find Documents
+ *
+ * Find data sets using chosen queries
+ *
+ * @param string $collection
+ * @param \Utopia\Database\Query[] $queries
+ * @param int $limit
+ * @param int $offset
+ * @param array $orderAttributes
+ * @param array $orderTypes
+ *
+ * @return Document[]
+ */
+ abstract public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = []): array;
+
+ /**
+ * Count Documents
+ *
+ * @param string $collection
+ * @param Query[] $queries
+ * @param int $max
+ *
+ * @return int
+ */
+ abstract public function count(string $collection, array $queries = [], int $max = 0): int;
+
+ /**
+ * Get max STRING limit
+ *
+ * @return int
+ */
+ abstract public function getStringLimit(): int;
+
+ /**
+ * Get max INT limit
+ *
+ * @return int
+ */
+ abstract public function getIntLimit(): int;
+
+ /**
+ * Is index supported?
+ *
+ * @return bool
+ */
+ abstract public function getSupportForIndex(): bool;
+
+ /**
+ * Is unique index supported?
+ *
+ * @return bool
+ */
+ abstract public function getSupportForUniqueIndex(): bool;
+
+ /**
+ * Is fulltext index supported?
+ *
+ * @return bool
+ */
+ abstract public function getSupportForFulltextIndex(): bool;
+
+ /**
+ * Filter Keys
+ *
+ * @throws Exception
+ * @return string
+ */
+ public function filter(string $value):string
+ {
+ $value = preg_replace("/[^A-Za-z0-9]_/", '', $value);
+
+ if(\is_null($value)) {
+ throw new Exception('Failed to filter key');
+ }
+
+ return $value;
+ }
+}
diff --git a/src/Database/Adapter/Cockroach.php b/src/Database/Adapter/Cockroach.php
new file mode 100644
index 000000000..9f1b6176a
--- /dev/null
+++ b/src/Database/Adapter/Cockroach.php
@@ -0,0 +1,8 @@
+pdo = $pdo;
+ }
+
+ /**
+ * Create Database
+ *
+ * @return bool
+ */
+ public function create(): bool
+ {
+ $name = $this->getNamespace();
+
+ return $this->getPDO()
+ ->prepare("CREATE DATABASE {$name} /*!40100 DEFAULT CHARACTER SET utf8mb4 */;")
+ ->execute();
+ }
+
+ /**
+ * Check if database exists
+ *
+ * @return bool
+ */
+ public function exists(): bool
+ {
+ $name = $this->getNamespace();
+
+ $stmt = $this->getPDO()
+ ->prepare("SELECT SCHEMA_NAME
+ FROM INFORMATION_SCHEMA.SCHEMATA
+ WHERE SCHEMA_NAME = :schema;");
+
+ $stmt->bindValue(':schema', $name, PDO::PARAM_STR);
+
+ $stmt->execute();
+
+ $document = $stmt->fetch();
+
+ return (($document['SCHEMA_NAME'] ?? '') == $name);
+ }
+
+ /**
+ * List Databases
+ *
+ * @return array
+ */
+ public function list(): array
+ {
+ $list = [];
+ return $list;
+ }
+
+ /**
+ * Delete Database
+ *
+ * @return bool
+ */
+ public function delete(): bool
+ {
+ $name = $this->getNamespace();
+
+ return $this->getPDO()
+ ->prepare("DROP DATABASE {$name};")
+ ->execute();
+ }
+
+ /**
+ * Create Collection
+ *
+ * @param string $id
+ * @return bool
+ */
+ public function createCollection(string $id): bool
+ {
+ $id = $this->filter($id);
+
+ $this->getPDO()
+ ->prepare("CREATE TABLE IF NOT EXISTS {$this->getNamespace()}.{$id}_permissions (
+ `_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `_uid` CHAR(255) NOT NULL,
+ `_action` CHAR(128) NOT NULL,
+ `_role` CHAR(128) NOT NULL,
+ PRIMARY KEY (`_id`),
+ INDEX `_index1` (`_uid`),
+ INDEX `_index2` (`_action` ASC, `_role` ASC)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;")
+ ->execute();
+
+ return $this->getPDO()
+ ->prepare("CREATE TABLE IF NOT EXISTS {$this->getNamespace()}.{$id} (
+ `_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `_uid` CHAR(255) NOT NULL,
+ `_permissions` TEXT NOT NULL,
+ PRIMARY KEY (`_id`),
+ UNIQUE KEY `_index1` (`_uid`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;")
+ ->execute();
+ }
+
+ /**
+ * Delete Collection
+ *
+ * @param string $id
+ * @return bool
+ */
+ public function deleteCollection(string $id): bool
+ {
+ $id = $this->filter($id);
+
+ $this->getPDO()
+ ->prepare("DROP TABLE {$this->getNamespace()}.{$id}_permissions;")
+ ->execute();
+
+ return $this->getPDO()
+ ->prepare("DROP TABLE {$this->getNamespace()}.{$id};")
+ ->execute();
+ }
+
+ /**
+ * Create Attribute
+ *
+ * @param string $collection
+ * @param string $id
+ * @param string $type
+ * @param int $size
+ * @param bool $array
+ *
+ * @return bool
+ */
+ public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false): bool
+ {
+ $name = $this->filter($collection);
+ $id = $this->filter($id);
+ $type = $this->getSQLType($type, $size, $signed);
+
+ if($array) {
+ $type = 'LONGTEXT';
+ }
+
+ return $this->getPDO()
+ ->prepare("ALTER TABLE {$this->getNamespace()}.{$name}
+ ADD COLUMN `{$id}` {$type};")
+ ->execute();
+ }
+
+ /**
+ * Delete Attribute
+ *
+ * @param string $collection
+ * @param string $id
+ * @param bool $array
+ *
+ * @return bool
+ */
+ public function deleteAttribute(string $collection, string $id, bool $array = false): bool
+ {
+ $name = $this->filter($collection);
+ $id = $this->filter($id);
+
+ return $this->getPDO()
+ ->prepare("ALTER TABLE {$this->getNamespace()}.{$name}
+ DROP COLUMN `{$id}`;")
+ ->execute();
+ }
+
+ /**
+ * Create Index
+ *
+ * @param string $collection
+ * @param string $id
+ * @param string $type
+ * @param array $attributes
+ * @param array $lengths
+ * @param array $orders
+ *
+ * @return bool
+ */
+ public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool
+ {
+ $name = $this->filter($collection);
+ $id = $this->filter($id);
+ $type = $this->getSQLIndex($type);
+
+ foreach($attributes as $key => &$attribute) {
+ $length = $lengths[$key] ?? '';
+ $length = (empty($length)) ? '' : '('.(int)$length.')';
+ $order = $orders[$key] ?? 'ASC';
+ $attribute = $this->filter($attribute);
+
+ $attribute = "`{$attribute}`{$length} {$order}";
+ }
+
+ return $this->getPDO()
+ ->prepare("CREATE {$type} `{$id}` ON {$this->getNamespace()}.{$name} (".implode(', ', $attributes).");")
+ ->execute();
+ }
+
+ /**
+ * Delete Index
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return bool
+ */
+ public function deleteIndex(string $collection, string $id): bool
+ {
+ $name = $this->filter($collection);
+ $id = $this->filter($id);
+
+ return $this->getPDO()
+ ->prepare("ALTER TABLE {$this->getNamespace()}.{$name}
+ DROP INDEX `{$id}`;")
+ ->execute();
+ }
+
+ /**
+ * Get Document
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return Document
+ */
+ public function getDocument(string $collection, string $id): Document
+ {
+ $name = $this->filter($collection);
+
+ $stmt = $this->getPDO()->prepare("SELECT * FROM {$this->getNamespace()}.{$name}
+ WHERE _uid = :_uid
+ LIMIT 1;
+ ");
+
+ $stmt->bindValue(':_uid', $id, PDO::PARAM_STR);
+
+ $stmt->execute();
+
+ $document = $stmt->fetch();
+
+ if(empty($document)) {
+ return new Document([]);
+ }
+
+ $permissions = (isset($document['_permissions'])) ? json_decode($document['_permissions'], true) : [];
+ $document['$id'] = $document['_uid'];
+ $document['$read'] = $permissions[Database::PERMISSION_READ] ?? [];
+ $document['$write'] = $permissions[Database::PERMISSION_WRITE] ?? [];
+ unset($document['_id']);
+ unset($document['_uid']);
+ unset($document['_permissions']);
+
+ return new Document($document);
+ }
+
+ /**
+ * Create Document
+ *
+ * @param string $collection
+ * @param Document $document
+ *
+ * @return Document
+ */
+ public function createDocument(string $collection, Document $document): Document
+ {
+ $attributes = $document->getAttributes();
+ $name = $this->filter($collection);
+ $columns = '';
+
+ /**
+ * Insert Attributes
+ */
+ foreach ($attributes as $attribute => $value) { // Parse statement
+ $column = $this->filter($attribute);
+ $columns .= "`{$column}`" . '=:' . $column . ',';
+ }
+
+ $stmt = $this->getPDO()
+ ->prepare("INSERT INTO {$this->getNamespace()}.{$name}
+ SET {$columns} _uid = :_uid, _permissions = :_permissions");
+
+ $stmt->bindValue(':_uid', $document->getId(), PDO::PARAM_STR);
+ $stmt->bindValue(':_permissions', json_encode([Database::PERMISSION_READ => $document->getRead(), Database::PERMISSION_WRITE => $document->getWrite()]), PDO::PARAM_STR);
+
+ foreach ($attributes as $attribute => $value) {
+ if(is_array($value)) { // arrays & objects should be saved as strings
+ $value = json_encode($value);
+ }
+
+ $attribute = $this->filter($attribute);
+ $value = (is_bool($value)) ? (int)$value : $value;
+ $stmt->bindValue(':' . $attribute, $value, $this->getPDOType($value));
+ }
+
+ try {
+ $stmt->execute();
+ } catch (PDOException $e) {
+ switch ($e->getCode()) {
+ case 1062:
+ case 23000:
+ throw new Duplicate('Duplicated document: '.$e->getMessage()); // TODO add test for catching this exception
+ break;
+
+ default:
+ throw $e;
+ break;
+ }
+ }
+
+ /**
+ * Insert Permissions
+ */
+ $stmt = $this->getPDO()
+ ->prepare("INSERT INTO {$this->getNamespace()}.{$name}_permissions
+ SET _uid = :_uid, _action = :_action, _role = :_role");
+
+ $stmt->bindValue(':_uid', $document->getId(), PDO::PARAM_STR);
+
+ foreach (['read' => $document->getRead(), 'write' => $document->getWrite()] as $action => $roles) { // Insert all permissions
+ foreach ($roles as $key => $role) {
+ $stmt->bindValue(':_action', $action, PDO::PARAM_STR);
+ $stmt->bindValue(':_role', $role, PDO::PARAM_STR);
+
+ if(!$stmt->execute()) {
+ throw new Exception('Failed to save permission');
+ }
+ }
+ }
+
+ return $document;
+ }
+
+ /**
+ * Update Document
+ *
+ * @param string $collection
+ * @param Document $document
+ *
+ * @return Document
+ */
+ public function updateDocument(string $collection, Document $document): Document
+ {
+ $attributes = $document->getAttributes();
+ $name = $this->filter($collection);
+ $columns = '';
+
+ /**
+ * Update Attributes
+ */
+ foreach ($attributes as $attribute => $value) { // Parse statement
+ $column = $this->filter($attribute);
+ $columns .= "`{$column}`" . '=:' . $column . ',';
+ }
+
+ $stmt = $this->getPDO()
+ ->prepare("UPDATE {$this->getNamespace()}.{$name}
+ SET {$columns} _uid = :_uid, _permissions = :_permissions WHERE _uid = :_uid");
+
+ $stmt->bindValue(':_uid', $document->getId(), PDO::PARAM_STR);
+ $stmt->bindValue(':_permissions', json_encode([Database::PERMISSION_READ => $document->getRead(), Database::PERMISSION_WRITE => $document->getWrite()]), PDO::PARAM_STR);
+
+ foreach ($attributes as $attribute => $value) {
+ if(is_array($value)) { // arrays & objects should be saved as strings
+ $value = json_encode($value);
+ }
+
+ $attribute = $this->filter($attribute);
+ $value = (is_bool($value)) ? (int)$value : $value;
+ $stmt->bindValue(':' . $attribute, $value, $this->getPDOType($value));
+ }
+
+ if(!empty($attributes)) {
+ $stmt->execute();
+ }
+
+ /**
+ * Update Permissions
+ */
+ $stmt = $this->getPDO() // Clean all old permissions to avoid any duplications
+ ->prepare("DELETE FROM {$this->getNamespace()}.{$name}_permissions
+ WHERE _uid = :_uid");
+
+ $stmt->bindValue(':_uid', $document->getId(), PDO::PARAM_STR);
+
+ if(!$stmt->execute()) {
+ throw new Exception('Failed to clean permissions');
+ }
+
+ $stmt = $this->getPDO()
+ ->prepare("INSERT INTO {$this->getNamespace()}.{$name}_permissions
+ SET _uid = :_uid, _action = :_action, _role = :_role");
+
+ $stmt->bindValue(':_uid', $document->getId(), PDO::PARAM_STR);
+
+ foreach (['read' => $document->getRead(), 'write' => $document->getWrite()] as $action => $roles) { // Insert all permissions
+ foreach ($roles as $key => $role) {
+ $stmt->bindValue(':_action', $action, PDO::PARAM_STR);
+ $stmt->bindValue(':_role', $role, PDO::PARAM_STR);
+
+ if(!$stmt->execute()) {
+ throw new Exception('Failed to save permission');
+ }
+ }
+ }
+
+ return $document;
+ }
+
+ /**
+ * Delete Document
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return bool
+ */
+ public function deleteDocument(string $collection, string $id): bool
+ {
+ $name = $this->filter($collection);
+
+ $stmt = $this->getPDO()
+ ->prepare("DELETE FROM {$this->getNamespace()}.{$name}
+ WHERE _uid = :_uid LIMIT 1");
+
+ $stmt->bindValue(':_uid', $id, PDO::PARAM_STR);
+
+ if(!$stmt->execute()) {
+ throw new Exception('Failed to clean document');
+ }
+
+ $stmt = $this->getPDO()
+ ->prepare("DELETE FROM {$this->getNamespace()}.{$name}_permissions
+ WHERE _uid = :_uid");
+
+ $stmt->bindValue(':_uid', $id, PDO::PARAM_STR);
+
+ if(!$stmt->execute()) {
+ throw new Exception('Failed to clean permissions');
+ }
+
+ return true;
+ }
+
+ /**
+ * Find Documents
+ *
+ * Find data sets using chosen queries
+ *
+ * @param string $collection
+ * @param \Utopia\Database\Query[] $queries
+ * @param int $limit
+ * @param int $offset
+ * @param array $orderAttributes
+ * @param array $orderTypes
+ * @param bool $count
+ *
+ * @return Document[]
+ */
+ public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = []): array
+ {
+ $name = $this->filter($collection);
+ $roles = Authorization::getRoles();
+ $where = ['1=1'];
+ $orders = [];
+
+ foreach($roles as &$role) {
+ $role = $this->getPDO()->quote($role, PDO::PARAM_STR);
+ }
+
+ foreach($orderAttributes as $i => $attribute) {
+ $attribute = $this->filter($attribute);
+ $orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC);
+ $orders[] = $attribute.' '.$orderType;
+ }
+
+ $permissions = (Authorization::$status) ? "INNER JOIN {$this->getNamespace()}.{$name}_permissions as table_permissions
+ ON table_main._uid = table_permissions._uid
+ AND table_permissions._action = 'read' AND table_permissions._role IN (".implode(',', $roles).")" : ''; // Disable join when no authorization required
+
+ foreach($queries as $i => $query) {
+ $conditions = [];
+ foreach ($query->getValues() as $key => $value) {
+ $conditions[] = 'table_main.'.$query->getAttribute().' '.$this->getSQLOperator($query->getOperator()).' :attribute_'.$i.'_'.$key.'_'.$query->getAttribute(); // Using `attrubute_` to avoid conflicts with custom names
+ }
+
+ $where[] = implode(' OR ', $conditions);
+ }
+
+ $order = (!empty($orders)) ? 'ORDER BY '.implode(', ', $orders) : '';
+
+ $stmt = $this->getPDO()->prepare("SELECT table_main.* FROM {$this->getNamespace()}.{$name} table_main
+ {$permissions}
+ WHERE ".implode(' AND ', $where)."
+ GROUP BY table_main._uid
+ {$order}
+ LIMIT :offset, :limit;
+ ");
+
+ foreach($queries as $i => $query) {
+ foreach($query->getValues() as $key => $value) {
+ $stmt->bindValue(':attribute_'.$i.'_'.$key.'_'.$query->getAttribute(), $value, $this->getPDOType($value));
+ }
+ }
+
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
+ $stmt->execute();
+
+ $results = $stmt->fetchAll();
+
+ foreach ($results as &$value) {
+ $permissions = (isset($value['_permissions'])) ? json_decode($value['_permissions'], true) : [];
+ $value['$id'] = $value['_uid'];
+ $value['$read'] = $permissions[Database::PERMISSION_READ] ?? [];
+ $value['$write'] = $permissions[Database::PERMISSION_WRITE] ?? [];
+ unset($value['_id']);
+ unset($value['_uid']);
+ unset($value['_permissions']);
+
+ $value = new Document($value);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Count Documents
+ *
+ * Count data set size using chosen queries
+ *
+ * @param string $collection
+ * @param \Utopia\Database\Query[] $queries
+ * @param int $max
+ *
+ * @return int
+ */
+ public function count(string $collection, array $queries = [], int $max = 0): int
+ {
+ $name = $this->filter($collection);
+ $roles = Authorization::getRoles();
+ $where = ['1=1'];
+ $limit = ($max === 0) ? '' : 'LIMIT :max';
+
+ foreach($roles as &$role) {
+ $role = $this->getPDO()->quote($role, PDO::PARAM_STR);
+ }
+
+ $permissions = (Authorization::$status) ? "INNER JOIN {$this->getNamespace()}.{$name}_permissions as table_permissions
+ ON table_main._uid = table_permissions._uid
+ AND table_permissions._action = 'read' AND table_permissions._role IN (".implode(',', $roles).")" : ''; // Disable join when no authorization required
+
+ foreach($queries as $i => $query) {
+ $conditions = [];
+ foreach ($query->getValues() as $key => $value) {
+ $conditions[] = 'table_main.'.$query->getAttribute().' '.$this->getSQLOperator($query->getOperator()).' :attribute_'.$i.'_'.$key.'_'.$query->getAttribute(); // Using `attrubute_` to avoid conflicts with custom names
+ }
+
+ $where[] = implode(' OR ', $conditions);
+ }
+
+ $stmt = $this->getPDO()->prepare("SELECT COUNT(1) as sum FROM (SELECT 1 FROM {$this->getNamespace()}.{$name} table_main
+ {$permissions}
+ WHERE ".implode(' AND ', $where)."
+ GROUP BY table_main._uid
+ {$limit}) table_count
+ ");
+
+ foreach($queries as $i => $query) {
+ foreach($query->getValues() as $key => $value) {
+ $stmt->bindValue(':attribute_'.$i.'_'.$key.'_'.$query->getAttribute(), $value, $this->getPDOType($value));
+ }
+ }
+
+ if($max !== 0) {
+ $stmt->bindValue(':max', $max, PDO::PARAM_INT);
+ }
+
+ $stmt->execute();
+
+ $result = $stmt->fetch();
+
+ return $result['sum'] ?? 0;
+ }
+
+ /**
+ * Get max STRING limit
+ *
+ * @return int
+ */
+ public function getStringLimit(): int
+ {
+ return 4294967295;
+ }
+
+ /**
+ * Get max INT limit
+ *
+ * @return int
+ */
+ public function getIntLimit(): int
+ {
+ return 4294967295;
+ }
+
+ /**
+ * Is index supported?
+ *
+ * @return bool
+ */
+ public function getSupportForIndex(): bool
+ {
+ return true;
+ }
+
+ /**
+ * Is unique index supported?
+ *
+ * @return bool
+ */
+ public function getSupportForUniqueIndex(): bool
+ {
+ return true;
+ }
+
+ /**
+ * Is fulltext index supported?
+ *
+ * @return bool
+ */
+ public function getSupportForFulltextIndex(): bool
+ {
+ return true;
+ }
+
+ /**
+ * Get SQL Type
+ *
+ * @param string $type
+ * @param int $size in chars
+ *
+ * @return string
+ */
+ protected function getSQLType(string $type, int $size, bool $signed = true): string
+ {
+ switch ($type) {
+ case Database::VAR_STRING:
+ // $size = $size * 4; // Convert utf8mb4 size to bytes
+ if($size > 16777215) {
+ return 'LONGTEXT';
+ }
+
+ if($size > 65535) {
+ return 'MEDIUMTEXT';
+ }
+
+ if($size > 16383) {
+ return 'TEXT';
+ }
+
+ return "VARCHAR({$size})";
+ break;
+
+ case Database::VAR_INTEGER: // We don't support zerofill: https://stackoverflow.com/a/5634147/2299554
+ $signed = ($signed) ? '' : ' UNSIGNED';
+ return 'INT'.$signed;
+ break;
+
+ case Database::VAR_FLOAT:
+ $signed = ($signed) ? '' : ' UNSIGNED';
+ return 'FLOAT'.$signed;
+ break;
+
+ case Database::VAR_BOOLEAN:
+ return 'TINYINT(1)';
+ break;
+
+ case Database::VAR_DOCUMENT:
+ return 'CHAR(255)';
+ break;
+
+ default:
+ throw new Exception('Unknown Type');
+ break;
+ }
+ }
+
+ /**
+ * Get SQL Operator
+ *
+ * @param string $operator
+ *
+ * @return string
+ */
+ protected function getSQLOperator(string $operator): string
+ {
+ switch ($operator) {
+ case Query::TYPE_EQUAL:
+ return '=';
+ break;
+
+ case Query::TYPE_NOTEQUAL:
+ return '!=';
+ break;
+
+ case Query::TYPE_LESSER:
+ return '<';
+ break;
+
+ case Query::TYPE_LESSEREQUAL:
+ return '<=';
+ break;
+
+ case Query::TYPE_GREATER:
+ return '>';
+ break;
+
+ case Query::TYPE_GREATEREQUAL:
+ return '>=';
+ break;
+
+ default:
+ throw new Exception('Unknown Operator:' . $operator);
+ break;
+ }
+ }
+
+ /**
+ * Get SQL Index
+ *
+ * @param string $operator
+ *
+ * @return string
+ */
+ protected function getSQLIndex(string $type): string
+ {
+ switch ($type) {
+ case Database::INDEX_KEY:
+ return 'INDEX';
+ break;
+
+ case Database::INDEX_UNIQUE:
+ return 'UNIQUE INDEX';
+ break;
+
+ default:
+ throw new Exception('Unknown Index Type:' . $type);
+ break;
+ }
+ }
+
+ /**
+ * Get PDO Type
+ *
+ * @param mixed $value
+ *
+ * @return int
+ */
+ protected function getPDOType($value): int
+ {
+ switch (gettype($value)) {
+ case 'string':
+ return PDO::PARAM_STR;
+ break;
+
+ case 'boolean':
+ return PDO::PARAM_INT;
+ break;
+
+ //case 'float': // (for historical reasons "double" is returned in case of a float, and not simply "float")
+ case 'double':
+ return PDO::PARAM_STR;
+ break;
+
+ case 'integer':
+ return PDO::PARAM_INT;
+ break;
+
+ case 'NULL':
+ return PDO::PARAM_NULL;
+ break;
+
+ default:
+ throw new Exception('Unknown PDO Type for ' . gettype($value));
+ break;
+ }
+ }
+
+ /**
+ * @return PDO
+ *
+ * @throws Exception
+ */
+ protected function getPDO()
+ {
+ return $this->pdo;
+ }
+}
\ No newline at end of file
diff --git a/src/Database/Adapter/MongoDB.php b/src/Database/Adapter/MongoDB.php
new file mode 100644
index 000000000..8de616650
--- /dev/null
+++ b/src/Database/Adapter/MongoDB.php
@@ -0,0 +1,134 @@
+client = $client;
+// }
+
+// /**
+// * Create Database
+// *
+// * @return bool
+// */
+// public function create(): bool
+// {
+// $namespace = $this->getNamespace();
+// return (!!$this->client->$namespace);
+// }
+
+// /**
+// * List Databases
+// *
+// * @return array
+// */
+// public function list(): array
+// {
+// $list = [];
+
+// foreach ($this->client->listDatabaseNames() as $key => $value) {
+// $list[] = $value;
+// }
+
+// return $list;
+// }
+
+// /**
+// * Delete Database
+// *
+// * @return bool
+// */
+// public function delete(): bool
+// {
+// return (!!$this->getDatabase()->dropCollection($this->getNamespace()));
+// }
+
+// /**
+// * Create Collection
+// *
+// * @param string $id
+// * @return bool
+// */
+// public function createCollection(string $id): bool
+// {
+// return (!!$this->getDatabase()->createCollection($id));
+// }
+
+// /**
+// * List Collections
+// *
+// * @return array
+// */
+// public function listCollections(): array
+// {
+// $list = [];
+
+// foreach ($this->getDatabase()->listCollectionNames() as $key => $value) {
+// $list[] = $value;
+// }
+
+// return $list;
+// }
+
+// /**
+// * Delete Collection
+// *
+// * @param string $id
+// * @return bool
+// */
+// public function deleteCollection(string $id): bool
+// {
+// return (!!$this->getDatabase()->dropCollection($id));
+// }
+
+// /**
+// * @return Database
+// *
+// * @throws Exception
+// */
+// protected function getDatabase()
+// {
+// if($this->database) {
+// return $this->database;
+// }
+
+// $namespace = $this->getNamespace();
+
+// return $this->client->$namespace;
+// }
+
+// /**
+// * @return Client
+// *
+// * @throws Exception
+// */
+// protected function getClient()
+// {
+// return $this->client;
+// }
+// }
\ No newline at end of file
diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php
new file mode 100644
index 000000000..589fe3e4b
--- /dev/null
+++ b/src/Database/Adapter/MySQL.php
@@ -0,0 +1,8 @@
+pdo = $pdo;
+// }
+
+// /**
+// * Create Database
+// *
+// * @return bool
+// */
+// public function create(): bool
+// {
+// $name = $this->getNamespace();
+
+// return $this->getPDO()
+// ->prepare("CREATE SCHEMA {$name} /*!40100 DEFAULT CHARACTER SET utf8mb4 */;")
+// ->execute();
+// }
+
+// /**
+// * List Databases
+// *
+// * @return array
+// */
+// public function list(): array
+// {
+// $stmt = $this->getPDO()
+// ->prepare("SELECT datname FROM pg_database;");
+
+// $stmt->execute();
+
+// $list = [];
+
+// foreach ($stmt->fetchAll() as $key => $value) {
+// $list[] = $value['datname'] ?? '';
+// }
+
+// return $list;
+// }
+
+// /**
+// * Delete Database
+// *
+// * @return bool
+// */
+// public function delete(): bool
+// {
+// $name = $this->getNamespace();
+
+// return $this->getPDO()
+// ->prepare("DROP SCHEMA {$name};")
+// ->execute();
+// }
+
+// /**
+// * Create Collection
+// *
+// * @param string $id
+// * @return bool
+// */
+// public function createCollection(string $id): bool
+// {
+// $name = $this->filter($id).'_documents';
+
+// return $this->getPDO()
+// ->prepare("CREATE TABLE {$this->getNamespace()}.{$name}(
+// _id INT PRIMARY KEY NOT NULL,
+// _uid CHAR(13) NOT NULL
+// );")
+// ->execute();
+// }
+
+// /**
+// * List Collections
+// *
+// * @return array
+// */
+// public function listCollections(): array
+// {
+// }
+
+// /**
+// * Delete Collection
+// *
+// * @param string $id
+// * @return bool
+// */
+// public function deleteCollection(string $id): bool
+// {
+// $name = $this->filter($id).'_documents';
+
+// return $this->getPDO()
+// ->prepare("DROP TABLE {$this->getNamespace()}.{$name};")
+// ->execute();
+// }
+
+// /**
+// * @return PDO
+// *
+// * @throws Exception
+// */
+// protected function getPDO()
+// {
+// return $this->pdo;
+// }
+// }
\ No newline at end of file
diff --git a/src/Database/Database.php b/src/Database/Database.php
new file mode 100644
index 000000000..b5272ec15
--- /dev/null
+++ b/src/Database/Database.php
@@ -0,0 +1,1111 @@
+ true,
+ self::VAR_INTEGER => true,
+ self::VAR_FLOAT => true,
+ self::VAR_BOOLEAN => true,
+ ];
+
+ /**
+ * Parent Collection
+ * Defines the structure for both system and custom collections
+ *
+ * @var array
+ */
+ protected $collection = [
+ '$id' => self::COLLECTIONS,
+ '$collection' => self::COLLECTIONS,
+ 'name' => 'collections',
+ 'attributes' => [
+ [
+ '$id' => 'name',
+ 'type' => self::VAR_STRING,
+ 'size' => 256,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'attributes',
+ 'type' => self::VAR_STRING,
+ 'size' => 1000000,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => ['json'],
+ ],
+ [
+ '$id' => 'indexes',
+ 'type' => self::VAR_STRING,
+ 'size' => 1000000,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => ['json'],
+ ],
+ [
+ '$id' => 'attributesInQueue',
+ 'type' => self::VAR_STRING,
+ 'size' => 1000000,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => ['json'],
+ ],
+ [
+ '$id' => 'indexesInQueue',
+ 'type' => self::VAR_STRING,
+ 'size' => 1000000,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => ['json'],
+ ],
+ ],
+ 'indexes' => [],
+ 'attributesInQueue' => [],
+ 'indexesInQueue' => [],
+ ];
+
+ /**
+ * @var array
+ */
+ static protected $filters = [];
+
+ /**
+ * @param Adapter $adapter
+ * @param Cache $cache
+ */
+ public function __construct(Adapter $adapter, Cache $cache)
+ {
+ $this->adapter = $adapter;
+ $this->cache = $cache;
+
+ self::addFilter('json',
+ /**
+ * @param mixed $value
+ * @return mixed
+ */
+ function($value) {
+ $value = ($value instanceof Document) ? $value->getArrayCopy() : $value;
+
+ if(!is_array($value) && !$value instanceof \stdClass) {
+ return $value;
+ }
+
+ return json_encode($value);
+ },
+ /**
+ * @param mixed $value
+ * @return mixed
+ */
+ function($value) {
+ if(!is_string($value)) {
+ return $value;
+ }
+
+ return json_decode($value, true);
+ }
+ );
+ }
+
+ /**
+ * Set Namespace.
+ *
+ * Set namespace to divide different scope of data sets
+ *
+ * @param string $namespace
+ *
+ * @return $this
+ *
+ * @throws Exception
+ */
+ public function setNamespace(string $namespace): self
+ {
+ $this->adapter->setNamespace($namespace);
+
+ return $this;
+ }
+
+ /**
+ * Get Namespace.
+ *
+ * Get namespace of current set scope
+ *
+ * @return string
+ *
+ * @throws Exception
+ */
+ public function getNamespace(): string
+ {
+ return $this->adapter->getNamespace();
+ }
+
+ /**
+ * Create Database
+ *
+ * @return bool
+ */
+ public function create(): bool
+ {
+ $this->adapter->create();
+
+ $this->createCollection(self::COLLECTIONS);
+ $this->createAttribute(self::COLLECTIONS, 'name', self::VAR_STRING, 512, true);
+ $this->createAttribute(self::COLLECTIONS, 'attributes', self::VAR_STRING, 1000000, false);
+ $this->createAttribute(self::COLLECTIONS, 'indexes', self::VAR_STRING, 1000000, false);
+ $this->createAttribute(self::COLLECTIONS, 'attributesInQueue', self::VAR_STRING, 1000000, false);
+ $this->createAttribute(self::COLLECTIONS, 'indexesInQueue', self::VAR_STRING, 1000000, false);
+ $this->createIndex(self::COLLECTIONS, '_key_1', self::INDEX_UNIQUE, ['name']);
+
+ return true;
+ }
+
+ /**
+ * Check if database exists
+ *
+ * @return bool
+ */
+ public function exists(): bool
+ {
+ return $this->adapter->exists();
+ }
+
+ /**
+ * List Databases
+ *
+ * @return array
+ */
+ public function list(): array
+ {
+ return $this->adapter->list();
+ }
+
+ /**
+ * Delete Database
+ *
+ * @return bool
+ */
+ public function delete(): bool
+ {
+ return $this->adapter->delete();
+ }
+
+ /**
+ * Create Collection
+ *
+ * @param string $id
+ *
+ * @return Document
+ */
+ public function createCollection(string $id): Document
+ {
+ $this->adapter->createCollection($id);
+
+ if($id === self::COLLECTIONS) {
+ return new Document($this->collection);
+ }
+
+ return $this->createDocument(Database::COLLECTIONS, new Document([
+ '$id' => $id,
+ '$read' => ['*'],
+ '$write' => ['*'],
+ 'name' => $id,
+ 'attributes' => [],
+ 'indexes' => [],
+ 'attributesInQueue' => [],
+ 'indexesInQueue' => [],
+ ]));
+ }
+
+ /**
+ * Get Collection
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return Document
+ */
+ public function getCollection(string $id): Document
+ {
+ return $this->getDocument(self::COLLECTIONS, $id);
+ }
+
+ /**
+ * List Collections
+ *
+ * @param int $offset
+ * @param int $limit
+ *
+ * @return array
+ */
+ public function listCollections($limit = 25, $offset = 0): array
+ {
+ Authorization::disable();
+
+ $result = $this->find(self::COLLECTIONS, [], $limit, $offset);
+
+ Authorization::reset();
+
+ return $result;
+ }
+
+ /**
+ * Delete Collection
+ *
+ * @param string $id
+ *
+ * @return bool
+ */
+ public function deleteCollection(string $id): bool
+ {
+ $this->adapter->deleteCollection($id);
+
+ return $this->deleteDocument(self::COLLECTIONS, $id);
+ }
+
+ /**
+ * Create Attribute
+ *
+ * @param string $collection
+ * @param string $id
+ * @param string $type
+ * @param int $size utf8mb4 chars length
+ * @param bool $required
+ * @param bool $signed
+ * @param bool $array
+ * @param array $filters
+ *
+ * @return bool
+ */
+ public function createAttribute(string $collection, string $id, string $type, int $size, bool $required, bool $signed = true, bool $array = false, array $filters = []): bool
+ {
+ $collection = $this->getCollection($collection);
+
+ $collection->setAttribute('attributes', new Document([
+ '$id' => $id,
+ 'type' => $type,
+ 'size' => $size,
+ 'required' => $required,
+ 'signed' => $signed,
+ 'array' => $array,
+ 'filters' => $filters,
+ ]), Document::SET_TYPE_APPEND);
+
+ if($collection->getId() !== self::COLLECTIONS) {
+ $this->updateDocument(self::COLLECTIONS, $collection->getId(), $collection);
+ }
+
+ switch ($type) {
+ case self::VAR_STRING:
+ if($size > $this->adapter->getStringLimit()) {
+ throw new Exception('Max size allowed for string is: '.number_format($this->adapter->getStringLimit()));
+ }
+ break;
+
+ case self::VAR_INTEGER:
+ $limit = ($signed) ? $this->adapter->getIntLimit() / 2 : $this->adapter->getIntLimit();
+ if($size > $limit) {
+ throw new Exception('Max size allowed for int is: '.number_format($limit));
+ }
+ break;
+ case self::VAR_FLOAT:
+ case self::VAR_BOOLEAN:
+ break;
+ default:
+ throw new Exception('Unknown attribute type: '.$type);
+ break;
+ }
+
+ return $this->adapter->createAttribute($collection->getId(), $id, $type, $size, $signed, $array);
+ }
+
+ /**
+ * Delete Attribute
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return bool
+ */
+ public function deleteAttribute(string $collection, string $id): bool
+ {
+ $collection = $this->getCollection($collection);
+
+ $attributes = $collection->getAttribute('attributes', []);
+
+ foreach ($attributes as $key => $value) {
+ if(isset($value['$id']) && $value['$id'] === $id) {
+ unset($attributes[$key]);
+ }
+ }
+
+ $collection->setAttribute('attributes', $attributes);
+
+ if($collection->getId() !== self::COLLECTIONS) {
+ $this->updateDocument(self::COLLECTIONS, $collection->getId(), $collection);
+ }
+
+ return $this->adapter->deleteAttribute($collection->getId(), $id);
+ }
+
+ /**
+ * Add Attribute in Queue
+ *
+ * @param string $collection
+ * @param string $id
+ * @param string $type
+ * @param int $size utf8mb4 chars length
+ * @param bool $required
+ * @param bool $signed
+ * @param bool $array
+ * @param array $filters
+ *
+ * @return bool
+ */
+ public function addAttributeInQueue(string $collection, string $id, string $type, int $size, bool $required, bool $signed = true, bool $array = false, array $filters = []): bool
+ {
+ $collection = $this->getCollection($collection);
+
+ $collection->setAttribute('attributesInQueue', new Document([
+ '$id' => $id,
+ 'type' => $type,
+ 'size' => $size,
+ 'required' => $required,
+ 'signed' => $signed,
+ 'array' => $array,
+ 'filters' => $filters,
+ ]), Document::SET_TYPE_APPEND);
+
+ if($collection->getId() !== self::COLLECTIONS) {
+ $this->updateDocument(self::COLLECTIONS, $collection->getId(), $collection);
+ }
+
+ return true;
+ }
+
+ /**
+ * Remove Attribute in Queue
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return bool
+ */
+ public function removeAttributeInQueue(string $collection, string $id): bool
+ {
+ $collection = $this->getCollection($collection);
+
+ $attributes = $collection->getAttribute('attributesInQueue', []);
+
+ foreach ($attributes as $key => $value) {
+ if(isset($value['$id']) && $value['$id'] === $id) {
+ unset($attributes[$key]);
+ }
+ }
+
+ $collection->setAttribute('attributesInQueue', $attributes);
+
+ if($collection->getId() !== self::COLLECTIONS) {
+ $this->updateDocument(self::COLLECTIONS, $collection->getId(), $collection);
+ }
+
+ return true;
+ }
+
+ /**
+ * Create Index
+ *
+ * @param string $collection
+ * @param string $id
+ * @param string $type
+ * @param array $attributes
+ * @param array $lengths
+ * @param array $orders
+ *
+ * @return bool
+ */
+ public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths = [], array $orders = []): bool
+ {
+ if(empty($attributes)) {
+ throw new Exception('Missing attributes');
+ }
+
+ $collection = $this->getCollection($collection);
+
+ $collection->setAttribute('indexes', new Document([
+ '$id' => $id,
+ 'type' => $type,
+ 'attributes' => $attributes,
+ 'lengths' => $lengths,
+ 'orders' => $orders,
+ ]), Document::SET_TYPE_APPEND);
+
+ if($collection->getId() !== self::COLLECTIONS) {
+ $this->updateDocument(self::COLLECTIONS, $collection->getId(), $collection);
+ }
+
+ switch ($type) {
+ case self::INDEX_KEY:
+ if(!$this->adapter->getSupportForIndex()) {
+ throw new Exception('Key index is not supported');
+ }
+ break;
+
+ case self::INDEX_UNIQUE:
+ if(!$this->adapter->getSupportForUniqueIndex()) {
+ throw new Exception('Unique index is not supported');
+ }
+ break;
+
+ case self::INDEX_FULLTEXT:
+ if(!$this->adapter->getSupportForUniqueIndex()) {
+ throw new Exception('Fulltext index is not supported');
+ }
+ break;
+
+ default:
+ throw new Exception('Unknown index type: '.$type);
+ break;
+ }
+
+ return $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders);
+ }
+
+ /**
+ * Delete Index
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return bool
+ */
+ public function deleteIndex(string $collection, string $id): bool
+ {
+ $collection = $this->getCollection($collection);
+
+ $indexes = $collection->getAttribute('indexes', []);
+
+ foreach ($indexes as $key => $value) {
+ if(isset($value['$id']) && $value['$id'] === $id) {
+ unset($indexes[$key]);
+ }
+ }
+
+ $collection->setAttribute('indexes', $indexes);
+
+ if($collection->getId() !== self::COLLECTIONS) {
+ $this->updateDocument(self::COLLECTIONS, $collection->getId(), $collection);
+ }
+
+ return $this->adapter->deleteIndex($collection->getId(), $id);
+ }
+
+ /**
+ * Add Index in Queue
+ *
+ * @param string $collection
+ * @param string $id
+ * @param string $type
+ * @param array $attributes
+ * @param array $lengths
+ * @param array $orders
+ *
+ * @return bool
+ */
+ public function addIndexInQueue(string $collection, string $id, string $type, array $attributes, array $lengths = [], array $orders = []): bool
+ {
+ if(empty($attributes)) {
+ throw new Exception('Missing attributes');
+ }
+
+ $collection = $this->getCollection($collection);
+
+ $collection->setAttribute('indexesInQueue', new Document([
+ '$id' => $id,
+ 'type' => $type,
+ 'attributes' => $attributes,
+ 'lengths' => $lengths,
+ 'orders' => $orders,
+ ]), Document::SET_TYPE_APPEND);
+
+ if($collection->getId() !== self::COLLECTIONS) {
+ $this->updateDocument(self::COLLECTIONS, $collection->getId(), $collection);
+ }
+
+ return true;
+ }
+
+ /**
+ * Remove Index in Queue
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return bool
+ */
+ public function removeIndexInQueue(string $collection, string $id): bool
+ {
+ $collection = $this->getCollection($collection);
+
+ $indexes = $collection->getAttribute('indexesInQueue', []);
+
+ foreach ($indexes as $key => $value) {
+ if(isset($value['$id']) && $value['$id'] === $id) {
+ unset($indexes[$key]);
+ }
+ }
+
+ $collection->setAttribute('indexesInQueue', $indexes);
+
+ if($collection->getId() !== self::COLLECTIONS) {
+ $this->updateDocument(self::COLLECTIONS, $collection->getId(), $collection);
+ }
+
+ return true;
+ }
+
+ /**
+ * Get Document
+ *
+ * @param string $collection
+ * @param string $id
+ *
+ * @return Document
+ */
+ public function getDocument(string $collection, string $id): Document
+ {
+ if($collection === self::COLLECTIONS && $id === self::COLLECTIONS) {
+ return new Document($this->collection);
+ }
+
+ if(empty($collection)) {
+ throw new Exception('test exception: '.$collection .':'. $id);
+ }
+
+ $collection = $this->getCollection($collection);
+ $document = null;
+ $cache = null;
+
+ // TODO@kodumbeats Check if returned cache id matches request
+ if ($cache = $this->cache->load('cache-'.$this->getNamespace().'-'.$collection->getId().'-'.$id, self::TTL)) {
+ $document = new Document($cache);
+ $validator = new Authorization($document, self::PERMISSION_READ);
+
+ if (!$validator->isValid($document->getRead()) && $collection->getId() !== self::COLLECTIONS) { // Check if user has read access to this document
+ return new Document();
+ }
+
+ if($document->isEmpty()) {
+ return $document;
+ }
+
+ return $document;
+ }
+
+ $document = $this->adapter->getDocument($collection->getId(), $id);
+
+ $document->setAttribute('$collection', $collection->getId());
+
+ $validator = new Authorization($document, self::PERMISSION_READ);
+
+ if (!$validator->isValid($document->getRead()) && $collection->getId() !== self::COLLECTIONS) { // Check if user has read access to this document
+ return new Document();
+ }
+
+ if($document->isEmpty()) {
+ return $document;
+ }
+
+ $document = $this->casting($collection, $document);
+ $document = $this->decode($collection, $document);
+
+ $this->cache->save('cache-'.$this->getNamespace().'-'.$collection->getId().'-'.$id, $document->getArrayCopy()); // save to cache after fetching from db
+
+ return $document;
+ }
+
+ /**
+ * Create Document
+ *
+ * @param string $collection
+ * @param Document $data
+ *
+ * @return Document
+ *
+ * @throws AuthorizationException
+ * @throws StructureException
+ */
+ public function createDocument(string $collection, Document $document): Document
+ {
+ $validator = new Authorization($document, self::PERMISSION_WRITE);
+
+ if (!$validator->isValid($document->getWrite())) { // Check if user has write access to this document
+ throw new AuthorizationException($validator->getDescription());
+ }
+
+ $collection = $this->getCollection($collection);
+
+ $document
+ ->setAttribute('$id', empty($document->getId()) ? $this->getId(): $document->getId())
+ ->setAttribute('$collection', $collection->getId())
+ ;
+
+ $document = $this->encode($collection, $document);
+
+ $validator = new Structure($collection);
+
+ if (!$validator->isValid($document)) {
+ throw new StructureException($validator->getDescription());
+ }
+
+ $document = $this->adapter->createDocument($collection->getId(), $document);
+
+ $document = $this->decode($collection, $document);
+
+ return $document;
+ }
+
+ /**
+ * Update Document
+ *
+ * @param string $collection
+ * @param string $id
+ * @param Document $document
+ *
+ * @return Document
+ *
+ * @throws Exception
+ */
+ public function updateDocument(string $collection, string $id, Document $document): Document
+ {
+ if (!$document->getId() || !$id) {
+ throw new Exception('Must define $id attribute');
+ }
+
+ $old = $this->getDocument($collection, $id); // TODO make sure user don\'t need read permission for write operations
+ $collection = $this->getCollection($collection);
+
+ // Make sure reserved keys stay constant
+ // $data['$id'] = $old->getId();
+ // $data['$collection'] = $old->getCollection();
+
+ $validator = new Authorization($old, 'write');
+
+ if (!$validator->isValid($old->getWrite())) { // Check if user has write access to this document
+ throw new AuthorizationException($validator->getDescription());
+ }
+
+ if (!$validator->isValid($document->getWrite())) { // Check if user has write access to this document
+ throw new AuthorizationException($validator->getDescription());
+ }
+
+ $document = $this->encode($collection, $document);
+
+ $validator = new Structure($collection);
+
+ if (!$validator->isValid($document)) { // Make sure updated structure still apply collection rules (if any)
+ throw new StructureException($validator->getDescription());
+ }
+
+ $document = $this->adapter->updateDocument($collection->getId(), $document);
+ $document = $this->decode($collection, $document);
+
+ $this->cache->purge('cache-'.$this->getNamespace().'-'.$collection->getId().'-'.$id);
+ $this->cache->save('cache-'.$this->getNamespace().'-'.$collection->getId().'-'.$id, $document->getArrayCopy());
+
+ return $document;
+ }
+
+ /**
+ * @param string $collection
+ * @param string $id
+ *
+ * @return bool
+ *
+ * @throws AuthorizationException
+ */
+ public function deleteDocument(string $collection, string $id): bool
+ {
+ $document = $this->getDocument($collection, $id);
+
+ $validator = new Authorization($document, 'write');
+
+ if (!$validator->isValid($document->getWrite())) { // Check if user has write access to this document
+ throw new AuthorizationException($validator->getDescription());
+ }
+
+ $this->cache->purge('cache-'.$this->getNamespace().'-'.$collection.'-'.$id);
+
+ return $this->adapter->deleteDocument($collection, $id);
+ }
+
+ /**
+ * Find Documents
+ *
+ * @param string $collection
+ * @param Query[] $queries
+ * @param int $limit
+ * @param int $offset
+ * @param array $orderAttributes
+ * @param array $orderTypes
+ *
+ * @return Document[]
+ */
+ public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = []): array
+ {
+ $collection = $this->getCollection($collection);
+
+ $results = $this->adapter->find($collection->getId(), $queries, $limit, $offset, $orderAttributes, $orderTypes);
+
+ foreach ($results as &$node) {
+ $node = $this->casting($collection, $node);
+ $node = $this->decode($collection, $node);
+ $node->setAttribute('$collection', $collection->getId());
+ }
+
+ return $results;
+ }
+
+ /**
+ * @param string $collection
+ * @param array $options
+ *
+ * @return Document|bool
+ */
+ public function findFirst(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [])
+ {
+ $results = $this->find($collection, $queries, $limit, $offset, $orderAttributes, $orderTypes);
+ return \reset($results);
+ }
+
+ /**
+ * @param string $collection
+ * @param array $options
+ *
+ * @return Document|false
+ */
+ public function findLast(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [])
+ {
+ $results = $this->find($collection, $queries, $limit, $offset, $orderAttributes, $orderTypes);
+ return \end($results);
+ }
+
+ /**
+ * Count Documents
+ *
+ * @param string $collection
+ * @param Query[] $queries
+ * @param int $max
+ *
+ * @return int
+ */
+ public function count(string $collection, array $queries = [], int $max = 0): int
+ {
+ $count = $this->adapter->count($collection, $queries, $max);
+
+ return $count;
+ }
+
+ // /**
+ // * @param array $data
+ // *
+ // * @return Document|false
+ // *
+ // * @throws Exception
+ // */
+ // public function overwriteDocument(array $data)
+ // {
+ // if (!isset($data['$id'])) {
+ // throw new Exception('Must define $id attribute');
+ // }
+
+ // $document = $this->getDocument($data['$collection'], $data['$id']); // TODO make sure user don\'t need read permission for write operations
+
+ // $validator = new Authorization($document, 'write');
+
+ // if (!$validator->isValid($document->getWrite())) { // Check if user has write access to this document
+ // throw new AuthorizationException($validator->getDescription());
+ // }
+
+ // $new = new Document($data);
+
+ // if (!$validator->isValid($new->getWrite())) { // Check if user has write access to this document
+ // throw new AuthorizationException($validator->getDescription());
+ // }
+
+ // $new = $this->encode($new);
+
+ // $validator = new Structure($this);
+
+ // if (!$validator->isValid($new)) { // Make sure updated structure still apply collection rules (if any)
+ // throw new StructureException($validator->getDescription());
+ // }
+
+ // $new = new Document($this->adapter->updateDocument($this->getCollection($new->getCollection()), $new->getId(), $new->getArrayCopy()));
+
+ // $new = $this->decode($new);
+
+ // return $new;
+ // }
+
+ /**
+ * Add Attribute Filter
+ *
+ * @param string $name
+ * @param callable $encode
+ * @param callable $decode
+ *
+ * @return void
+ */
+ static public function addFilter(string $name, callable $encode, callable $decode): void
+ {
+ self::$filters[$name] = [
+ 'encode' => $encode,
+ 'decode' => $decode,
+ ];
+ }
+
+ /**
+ * Encode Document
+ *
+ * @param Document $collection
+ * @param Document $document
+ *
+ * @return Document
+ */
+ public function encode(Document $collection, Document $document):Document
+ {
+ $attributes = $collection->getAttribute('attributes', []);
+
+ foreach ($attributes as $attribute) {
+ $key = $attribute['$id'] ?? '';
+ $array = $attribute['array'] ?? false;
+ $filters = $attribute['filters'] ?? [];
+ $value = $document->getAttribute($key, null);
+
+ if(is_null($value)) {
+ continue;
+ }
+
+ $value = ($array) ? $value : [$value];
+
+ foreach ($value as &$node) {
+ if (($node !== null)) {
+ foreach ($filters as $filter) {
+ $node = $this->encodeAttribute($filter, $node);
+ }
+ }
+ }
+
+ if(!$array) {
+ $value = $value[0];
+ }
+
+ $document->setAttribute($key, $value);
+ }
+
+ return $document;
+ }
+
+ /**
+ * Decode Document
+ *
+ * @param Document $collection
+ * @param Document $document
+ *
+ * @return Document
+ */
+ public function decode(Document $collection, Document $document):Document
+ {
+ $attributes = $collection->getAttribute('attributes', []);
+
+ foreach ($attributes as $attribute) {
+ $key = $attribute['$id'] ?? '';
+ $array = $attribute['array'] ?? false;
+ $filters = $attribute['filters'] ?? [];
+ $value = $document->getAttribute($key, null);
+
+ if(is_null($value)) {
+ continue;
+ }
+
+ $value = ($array) ? $value : [$value];
+
+ foreach ($value as &$node) {
+ if (($node !== null)) {
+ foreach (array_reverse($filters) as $filter) {
+ $node = $this->decodeAttribute($filter, $node);
+ }
+ }
+ }
+
+ $document->setAttribute($key, ($array) ? $value : $value[0]);
+ }
+
+ return $document;
+ }
+
+ /**
+ * Casting
+ *
+ * @param Document $collection
+ * @param Document $document
+ *
+ * @return Document
+ */
+ public function casting(Document $collection, Document $document):Document
+ {
+ $attributes = $collection->getAttribute('attributes', []);
+
+ foreach ($attributes as $attribute) {
+ $key = $attribute['$id'] ?? '';
+ $type = $attribute['type'] ?? '';
+ $array = $attribute['array'] ?? false;
+ $value = $document->getAttribute($key, null);
+
+ if($array) {
+ $value = (!is_string($value)) ? $value : json_decode($value, true);
+ }
+ else {
+ $value = [$value];
+ }
+
+ foreach ($value as &$node) {
+ switch ($type) {
+ case self::VAR_BOOLEAN:
+ $node = (bool)$node;
+ break;
+ case self::VAR_INTEGER:
+ $node = (int)$node;
+ break;
+ case self::VAR_FLOAT:
+ $node = (float)$node;
+ break;
+
+ default:
+ # code...
+ break;
+ }
+ }
+
+ $document->setAttribute($key, ($array) ? $value : $value[0]);
+ }
+
+ return $document;
+ }
+
+ /**
+ * Encode Attribute
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ protected function encodeAttribute(string $name, $value)
+ {
+ if (!isset(self::$filters[$name])) {
+ throw new Exception('Filter not found');
+ }
+
+ try {
+ $value = self::$filters[$name]['encode']($value);
+ } catch (\Throwable $th) {
+ throw $th;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Decode Attribute
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ protected function decodeAttribute(string $name, $value)
+ {
+ if (!isset(self::$filters[$name])) {
+ throw new Exception('Filter not found');
+ }
+
+ try {
+ $value = self::$filters[$name]['decode']($value);
+ } catch (\Throwable $th) {
+ throw $th;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get 13 Chars Unique ID.
+ *
+ * @return string
+ */
+ public function getId(): string
+ {
+ return \uniqid();
+ }
+}
diff --git a/src/Database/Document.php b/src/Database/Document.php
new file mode 100644
index 000000000..efed817c7
--- /dev/null
+++ b/src/Database/Document.php
@@ -0,0 +1,289 @@
+ &$value) {
+ if (\is_array($value)) {
+ if ((isset($value['$id']) || isset($value['$collection']))) {
+ $input[$key] = new self($value);
+ } else {
+ foreach ($value as $childKey => $child) {
+ if ((isset($child['$id']) || isset($child['$collection'])) && (!$child instanceof self)) {
+ $value[$childKey] = new self($child);
+ }
+ }
+ }
+ }
+ }
+
+ parent::__construct($input);
+ }
+
+ /**
+ * @return string
+ */
+ public function getId(): string
+ {
+ return $this->getAttribute('$id', '');
+ }
+
+ /**
+ * @return string
+ */
+ public function getCollection(): string
+ {
+ return $this->getAttribute('$collection', '');
+ }
+
+ /**
+ * @return array
+ */
+ public function getRead(): array
+ {
+ return $this->getAttribute('$read', []);
+ }
+
+ /**
+ * @return array
+ */
+ public function getWrite(): array
+ {
+ return $this->getAttribute('$write', []);
+ }
+
+ /**
+ * Get Document Attributes
+ *
+ * @return array
+ */
+ public function getAttributes(): array
+ {
+ $attributes = [];
+
+ foreach ($this as $attribute => $value) {
+ if(array_key_exists($attribute, ['$id' => true, '$collection' => true, '$read' => true, '$write' => []])) {
+ continue;
+ }
+
+ $attributes[$attribute] = $value;
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Get Attribute.
+ *
+ * Method for getting a specific fields attribute. If $name is not found $default value will be returned.
+ *
+ * @param string $name
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function getAttribute(string $name, $default = null)
+ {
+ $name = \explode('.', $name);
+
+ $temp = &$this;
+
+ foreach ($name as $key) {
+ if (!isset($temp[$key])) {
+ return $default;
+ }
+
+ $temp = &$temp[$key];
+ }
+
+ return $temp;
+ }
+
+ /**
+ * Set Attribute.
+ *
+ * Method for setting a specific field attribute
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param string $type
+ *
+ * @return self
+ */
+ public function setAttribute(string $key, $value, string $type = self::SET_TYPE_ASSIGN): self
+ {
+ switch ($type) {
+ case self::SET_TYPE_ASSIGN:
+ $this[$key] = $value;
+ break;
+ case self::SET_TYPE_APPEND:
+ $this[$key] = (!isset($this[$key]) || !\is_array($this[$key])) ? [] : $this[$key];
+ \array_push($this[$key], $value);
+ break;
+ case self::SET_TYPE_PREPEND:
+ $this[$key] = (!isset($this[$key]) || !\is_array($this[$key])) ? [] : $this[$key];
+ \array_unshift($this[$key], $value);
+ break;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove Attribute.
+ *
+ * Method for removing a specific field attribute
+ *
+ * @param string $key
+ *
+ * @return self
+ */
+ public function removeAttribute(string $key): self
+ {
+ if (isset($this[$key])) {
+ unset($this[$key]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Search.
+ *
+ * Get array child by key and value match
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param mixed $scope
+ *
+ * @return mixed
+ */
+ public function search(string $key, $value, $scope = null)
+ {
+ $array = (!\is_null($scope)) ? $scope : $this;
+
+ if (\is_array($array) || $array instanceof self) {
+ if (isset($array[$key]) && $array[$key] == $value) {
+ return $array;
+ }
+
+ foreach ($array as $k => $v) {
+ if ((\is_array($v) || $v instanceof self) && (!empty($v))) {
+ $result = $this->search($key, $value, $v);
+
+ if (!empty($result)) {
+ return $result;
+ }
+ } else {
+ if ($k === $key && $v === $value) {
+ return $array;
+ }
+ }
+ }
+ }
+
+ if ($array === $value) {
+ return $array;
+ }
+
+ return;
+ }
+
+ /**
+ * Checks if document has data.
+ *
+ * @return bool
+ */
+ public function isEmpty(): bool
+ {
+ return empty($this->getId());
+ }
+
+ /**
+ * Checks if a document key is set.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function isSet($key): bool
+ {
+ return isset($this[$key]);
+ }
+
+ /**
+ * Get Array Copy.
+ *
+ * Outputs entity as a PHP array
+ *
+ * @param array $allow
+ * @param array $disallow
+ *
+ * @return array
+ */
+ public function getArrayCopy(array $allow = [], array $disallow = []): array
+ {
+ $array = parent::getArrayCopy();
+
+ $output = [];
+
+ foreach ($array as $key => &$value) {
+ if (!empty($allow) && !\in_array($key, $allow)) { // Export only allow fields
+ continue;
+ }
+
+ if (!empty($disallow) && \in_array($key, $disallow)) { // Don't export disallowed fields
+ continue;
+ }
+
+ if ($value instanceof self) {
+ $output[$key] = $value->getArrayCopy($allow, $disallow);
+ } elseif (\is_array($value)) {
+ foreach ($value as $childKey => &$child) {
+ if ($child instanceof self) {
+ $output[$key][$childKey] = $child->getArrayCopy($allow, $disallow);
+ } else {
+ $output[$key][$childKey] = $child;
+ }
+ }
+
+ if (empty($value)) {
+ $output[$key] = $value;
+ }
+ } else {
+ $output[$key] = $value;
+ }
+ }
+
+ return $output;
+ }
+}
diff --git a/src/Database/Exception/Authorization.php b/src/Database/Exception/Authorization.php
new file mode 100644
index 000000000..1154b1ac1
--- /dev/null
+++ b/src/Database/Exception/Authorization.php
@@ -0,0 +1,7 @@
+attribute = $attribute;
+ $this->operator = $operator;
+ $this->values = $values;
+ }
+
+ /**
+ * Get attribute
+ *
+ * @return string
+ */
+ public function getAttribute(): string
+ {
+ return $this->attribute;
+ }
+
+ /**
+ * Get operator
+ *
+ * @return string
+ */
+ public function getOperator(): string
+ {
+ return $this->operator;
+ }
+
+ /**
+ * Get operand
+ *
+ * @return mixed
+ */
+ public function getValues()
+ {
+ return $this->values;
+ }
+
+ /**
+ * Get all query details as array
+ *
+ * @return array
+ */
+ public function getQuery(): array
+ {
+ return [
+ 'attribute' => $this->attribute,
+ 'operator' => $this->operator,
+ 'values' => $this->values,
+ ];
+ }
+
+ /**
+ * Parse query filter
+ *
+ * @param string $filter
+ *
+ * @return Query
+ * */
+ public static function parse(string $filter): Query
+ {
+ $attribute = '';
+ $operator = '';
+ $values = [];
+
+ // get index of open parentheses
+ /** @var int */
+ $end = mb_strpos($filter, '(');
+
+ // count stanzas by only counting '.' that come before open parentheses
+ /** @var int */
+ $stanzas = mb_substr_count(mb_substr($filter, 0, $end), ".") + 1;
+
+ // TODO@kodumbeats handle relations between collections, e.g. if($stanzas > 2)
+ switch ($stanzas) {
+ case 2:
+ // use limit param to ignore '.' in $expression
+ $input = explode('.', $filter, $stanzas);
+ $attribute = $input[0];
+ $expression = $input[1];
+ [$operator, $values] = self::parseExpression($expression);
+ break;
+ }
+
+ return new Query($attribute, $operator, $values);
+ }
+
+ /**
+ * Get attribute key-value from query expression
+ * $expression: string with format 'operator(value)'
+ *
+ * @param string $expression
+ *
+ * @return array
+ */
+ protected static function parseExpression(string $expression): array
+ {
+ //find location of parentheses in expression
+
+ /** @var int */
+ $start = mb_strpos($expression, '(');
+ /** @var int */
+ $end = mb_strrpos($expression, ')');
+
+ //extract the query method
+
+ /** @var string */
+ $operator = mb_substr($expression, 0, $start);
+
+ //grab everything inside parentheses
+
+ /** @var mixed */
+ $value = mb_substr($expression,
+ ($start + 1), /* exclude open paren*/
+ ($end - $start - 1) /* exclude closed paren*/
+ );
+
+ // Explode comma-separated values
+
+ $values = explode(',', $value);
+
+ // Cast $value type
+
+ /** @var array */
+ $values = array_map(function ($value) {
+
+ // Trim whitespace from around $value
+
+ $value = trim($value);
+
+ switch (true) {
+ // type casted to int or float by "+" operator
+ case is_numeric($value):
+ return $value + 0;
+
+ // since (bool)"false" returns true, check bools manually
+ case $value === 'true':
+ return true;
+
+ case $value === 'false':
+ return false;
+
+ // need special case to cast (null) as null, not string
+ case $value === 'null':
+ return null;
+
+ default:
+ // strip escape characters
+ $value = stripslashes($value);
+ // trim leading and tailing quotes
+ return trim($value, '\'"');
+ }
+
+ }, $values);
+
+ return [$operator, $values];
+ }
+}
diff --git a/src/Database/Validator/Authorization.php b/src/Database/Validator/Authorization.php
new file mode 100644
index 000000000..90bbec295
--- /dev/null
+++ b/src/Database/Validator/Authorization.php
@@ -0,0 +1,210 @@
+ true];
+
+ /**
+ * @var Document
+ */
+ protected $document;
+
+ /**
+ * @var string
+ */
+ protected $action = '';
+
+ /**
+ * @var string
+ */
+ protected $message = 'Authorization Error';
+
+ /**
+ * @param Document $document
+ * @param string $action
+ */
+ public function __construct(Document $document, $action)
+ {
+ $this->document = $document;
+ $this->action = $action;
+ }
+
+ /**
+ * Get Description.
+ *
+ * Returns validator description
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->message;
+ }
+
+ /**
+ * Is valid.
+ *
+ * Returns true if valid or false if not.
+ *
+ * @param mixed $permissions
+ *
+ * @return bool
+ */
+ public function isValid($permissions)
+ {
+ if (!self::$status) {
+ return true;
+ }
+
+ if(empty($permissions)) {
+ $this->message = 'No permissions provided for action \''.$this->action.'\'';
+ return false;
+ }
+
+ $permission = '-';
+
+ foreach ($permissions as $permission) {
+ if (\array_key_exists($permission, self::$roles)) {
+ return true;
+ }
+ }
+
+ $this->message = 'Missing "'.$this->action.'" permission for role "'.$permission.'". Only this scopes "'.\json_encode(self::getRoles()).'" are given and only this are allowed "'.\json_encode($permissions).'".';
+
+ return false;
+ }
+
+ /**
+ * @param string $role
+ * @return void
+ */
+ public static function setRole(string $role): void
+ {
+ self::$roles[$role] = true;
+ }
+
+ /**
+ * @param string $role
+ *
+ * @return void
+ */
+ public static function unsetRole(string $role): void
+ {
+ unset(self::$roles[$role]);
+ }
+
+ /**
+ * @return array
+ */
+ public static function getRoles(): array
+ {
+ return \array_keys(self::$roles);
+ }
+
+ /**
+ * @return void
+ */
+ public static function cleanRoles(): void
+ {
+ self::$roles = [];
+ }
+
+ /**
+ * @param string $role
+ *
+ * @return bool
+ */
+ public static function isRole(string $role): bool
+ {
+ return (\array_key_exists($role, self::$roles));
+ }
+
+ /**
+ * @var bool
+ */
+ public static $status = true;
+
+ /**
+ * Default value in case we need
+ * to reset Authorization status
+ *
+ * @var bool
+ */
+ public static $statusDefault = true;
+
+ /**
+ * Change default status.
+ * This will be used for the
+ * value set on the self::reset() method
+ *
+ * @param bool $status
+ * @return void
+ */
+ public static function setDefaultStatus(bool $status): void
+ {
+ self::$statusDefault = $status;
+ self::$status = $status;
+ }
+
+ /**
+ * Enable Authorization checks
+ *
+ * @return void
+ */
+ public static function enable(): void
+ {
+ self::$status = true;
+ }
+
+ /**
+ * Disable Authorization checks
+ *
+ * @return void
+ */
+ public static function disable(): void
+ {
+ self::$status = false;
+ }
+
+ /**
+ * Disable Authorization checks
+ *
+ * @return void
+ */
+ public static function reset(): void
+ {
+ self::$status = self::$statusDefault;
+ }
+
+ /**
+ * Is array
+ *
+ * Function will return true if object is array.
+ *
+ * @return bool
+ */
+ public function isArray(): bool
+ {
+ return false;
+ }
+
+ /**
+ * Get Type
+ *
+ * Returns validator type.
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return self::TYPE_ARRAY;
+ }
+}
diff --git a/src/Database/Validator/Collection.php b/src/Database/Validator/Collection.php
new file mode 100644
index 000000000..f39cae13a
--- /dev/null
+++ b/src/Database/Validator/Collection.php
@@ -0,0 +1,62 @@
+collections = $collections;
+// $this->merge = $merge;
+
+// return parent::__construct($database);
+// }
+
+// /**
+// * Is valid.
+// *
+// * Returns true if valid or false if not.
+// *
+// * @param mixed $document
+// *
+// * @return bool
+// */
+// public function isValid($document)
+// {
+// $document = new Document(
+// \array_merge($this->merge, ($document instanceof Document) ? $document->getArrayCopy() : $document)
+// );
+
+// if (\is_null($document->getCollection())) {
+// $this->message = 'Missing collection attribute $collection';
+
+// return false;
+// }
+
+// if (!\in_array($document->getCollection(), $this->collections)) {
+// $this->message = 'Collection is not allowed';
+
+// return false;
+// }
+
+// return parent::isValid($document);
+// }
+// }
diff --git a/src/Database/Validator/DocumentId.php b/src/Database/Validator/DocumentId.php
new file mode 100644
index 000000000..40837c1f9
--- /dev/null
+++ b/src/Database/Validator/DocumentId.php
@@ -0,0 +1,97 @@
+database = $database;
+ $this->collection = $collection;
+ }
+
+ /**
+ * Get Description.
+ *
+ * Returns validator description
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->message;
+ }
+
+ /**
+ * Is valid.
+ *
+ * Returns true if valid or false if not.
+ *
+ * @param $value
+ *
+ * @return bool
+ */
+ public function isValid($id)
+ {
+ $document = $this->database->getDocument($this->collection, $id);
+
+ if (!$document->getId()) {
+ return false;
+ }
+
+ if ($document->getCollection() !== $this->collection) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Is array
+ *
+ * Function will return true if object is array.
+ *
+ * @return bool
+ */
+ public function isArray(): bool
+ {
+ return false;
+ }
+
+ /**
+ * Get Type
+ *
+ * Returns validator type.
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return self::TYPE_STRING;
+ }
+}
diff --git a/src/Database/Validator/Key.php b/src/Database/Validator/Key.php
new file mode 100644
index 000000000..7bb42f0c1
--- /dev/null
+++ b/src/Database/Validator/Key.php
@@ -0,0 +1,79 @@
+message;
+ }
+
+ /**
+ * Is valid.
+ *
+ * Returns true if valid or false if not.
+ *
+ * @param $value
+ *
+ * @return bool
+ */
+ public function isValid($value)
+ {
+ if (!\is_string($value)) {
+ return false;
+ }
+
+ if(mb_substr($value, 0, 1) === '_') {
+ return false;
+ }
+
+ if (\preg_match('/[^A-Za-z0-9\-\_]/', $value)) {
+ return false;
+ }
+
+ if (\mb_strlen($value) > 32) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Is array
+ *
+ * Function will return true if object is array.
+ *
+ * @return bool
+ */
+ public function isArray(): bool
+ {
+ return false;
+ }
+
+ /**
+ * Get Type
+ *
+ * Returns validator type.
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return self::TYPE_STRING;
+ }
+}
diff --git a/src/Database/Validator/Permissions.php b/src/Database/Validator/Permissions.php
new file mode 100644
index 000000000..829ee70ec
--- /dev/null
+++ b/src/Database/Validator/Permissions.php
@@ -0,0 +1,76 @@
+message;
+ }
+
+ /**
+ * Is valid.
+ *
+ * Returns true if valid or false if not.
+ *
+ * @param mixed $roles
+ *
+ * @return bool
+ */
+ public function isValid($roles)
+ {
+ if(!is_array($roles)) {
+ $this->message = 'Permissions roles must be an array of strings';
+ return false;
+ }
+
+ foreach ($roles as $role) {
+ if (!\is_string($role)) {
+ $this->message = 'Permissions role must be of type string.';
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Is array
+ *
+ * Function will return true if object is array.
+ *
+ * @return bool
+ */
+ public function isArray(): bool
+ {
+ return false;
+ }
+
+ /**
+ * Get Type
+ *
+ * Returns validator type.
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return self::TYPE_ARRAY;
+ }
+}
diff --git a/src/Database/Validator/QueryValidator.php b/src/Database/Validator/QueryValidator.php
new file mode 100644
index 000000000..e3246a1b1
--- /dev/null
+++ b/src/Database/Validator/QueryValidator.php
@@ -0,0 +1,120 @@
+schema = $schema;
+ }
+
+ /**
+ * Get Description.
+ *
+ * Returns validator description
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->message;
+ }
+
+ /**
+ * Is valid.
+ *
+ * Returns true if query typed according to schema.
+ *
+ * @param $query
+ *
+ * @return bool
+ */
+ public function isValid($query)
+ {
+ // Validate operator
+ if (!in_array($query->getOperator(), $this->operators)) {
+ $this->message = 'Query operator invalid: ' . $query->getOperator();
+ return false;
+ }
+
+ // Search for attribute in schema
+ $attributeIndex = array_search($query->getAttribute(), array_column($this->schema, '$id'));
+
+ if ($attributeIndex === false) {
+ $this->message = 'Attribute not found in schema: ' . $query->getAttribute();
+ return false;
+ }
+
+ // Extract the type of desired attribute from collection $schema
+ $attributeType = $this->schema[array_search($query->getAttribute(), array_column($this->schema, '$id'))]['type'];
+
+ foreach ($query->getValues() as $value) {
+ if (gettype($value) !== $attributeType) {
+ $this->message = 'Query type does not match expected: ' . $attributeType;
+ return false;
+ }
+ }
+
+ return true;
+ }
+ /**
+ * Is array
+ *
+ * Function will return true if object is array.
+ *
+ * @return bool
+ */
+ public function isArray(): bool
+ {
+ return false;
+ }
+
+ /**
+ * Get Type
+ *
+ * Returns validator type.
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return self::TYPE_OBJECT;
+ }
+}
diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php
new file mode 100644
index 000000000..7824adbdc
--- /dev/null
+++ b/src/Database/Validator/Structure.php
@@ -0,0 +1,311 @@
+ '$id',
+ 'type' => Database::VAR_STRING,
+ 'size' => 64,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => '$collection',
+ 'type' => Database::VAR_STRING,
+ 'size' => 64,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => '$read',
+ 'type' => Database::VAR_STRING,
+ 'size' => 64,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => true,
+ 'filters' => [],
+ ],
+ [
+ '$id' => '$write',
+ 'type' => Database::VAR_STRING,
+ 'size' => 64,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => true,
+ 'filters' => [],
+ ],
+ ];
+
+ /**
+ * @var array
+ */
+ static protected $formats = [];
+
+ /**
+ * @var string
+ */
+ protected $message = 'General Error';
+
+ /**
+ * Structure constructor.
+ *
+ * @param Document $collection
+ */
+ public function __construct(Document $collection)
+ {
+ $this->collection = $collection;
+ }
+
+ /**
+ * Remove a Validator
+ *
+ * @param string $name
+ *
+ * @return array
+ */
+ static public function getFormats(): array
+ {
+ return self::$formats;
+ }
+
+ /**
+ * Add a new Validator
+ *
+ * @param string $name
+ * @param Validator $validator
+ * @param string $type
+ */
+ static public function addFormat(string $name, Validator $validator, string $type): void
+ {
+ self::$formats[$name] = [
+ 'validator' => $validator,
+ 'type' => $type,
+ ];
+ }
+
+ /**
+ * Get a Validator
+ *
+ * @param string $name
+ *
+ * @return Validator
+ */
+ static public function getFormat(string $name, string $type): Validator
+ {
+ if(isset(self::$formats[$name])) {
+ if(self::$formats[$name]['type'] !== $type) {
+ throw new Exception('Format ("'.$name.'") not available for this attribute type ("'.$type.'")');
+ }
+
+ return self::$formats[$name]['validator'];
+ }
+
+ throw new Exception('Unknown format validator: "'.$name.'"');
+ }
+
+ /**
+ * Remove a Validator
+ *
+ * @param string $name
+ */
+ static public function removeFormat(string $name): void
+ {
+ unset(self::$formats[$name]);
+ }
+
+ /**
+ * Get Description.
+ *
+ * Returns validator description
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return 'Invalid document structure: '.$this->message;
+ }
+
+ /**
+ * Is valid.
+ *
+ * Returns true if valid or false if not.
+ *
+ * @param mixed $document
+ *
+ * @return bool
+ */
+ public function isValid($document)
+ {
+ if(!$document instanceof Document) {
+ $this->message = 'Value must be an instance of Document';
+ return false;
+ }
+
+ if (empty($document->getCollection())) {
+ $this->message = 'Missing collection attribute $collection';
+ return false;
+ }
+
+ if (empty($this->collection->getId()) || Database::COLLECTIONS !== $this->collection->getCollection()) {
+ $this->message = 'Collection "'.$this->collection->getCollection().'" not found';
+ return false;
+ }
+
+ $keys = [];
+ $structure = $document->getArrayCopy();
+ $attributes = \array_merge($this->attributes, $this->collection->getAttribute('attributes', []));
+
+ foreach ($attributes as $key => $attribute) { // Check all required attributes are set
+ $name = $attribute['$id'] ?? '';
+ $required = $attribute['required'] ?? false;
+
+ $keys[$name] = $attribute; // List of allowed attributes to help find unknown ones
+
+ if($required && !isset($structure[$name])) {
+ $this->message = 'Missing required attribute "'.$name.'"';
+ return false;
+ }
+ }
+
+ foreach ($structure as $key => $value) {
+ if(!array_key_exists($key, $keys)) { // Check no unknown attributes are set
+ $this->message = 'Unknown attribute: "'. '"'.$key.'"';
+ return false;
+ }
+
+ $attribute = $keys[$key] ?? [];
+ $type = $attribute['type'] ?? '';
+ $array = $attribute['array'] ?? false;
+ $format = $attribute['format'] ?? '';
+ $required = $attribute['required'] ?? false;
+
+ switch ($type) {
+ case Database::VAR_STRING:
+ $validator = new Text(0);
+ break;
+
+ case Database::VAR_INTEGER:
+ $validator = new Integer();
+ break;
+
+ case Database::VAR_FLOAT:
+ $validator = new FloatValidator();
+ break;
+
+
+ case Database::VAR_BOOLEAN:
+ $validator = new Boolean();
+ break;
+
+ default:
+ $this->message = 'Unknown attribute type "'.$type.'"';
+ return false;
+ break;
+ }
+
+ if($array) { // Validate attribute type
+ if(!is_array($value)) {
+ $this->message = 'Attribute "'.$key.'" must be an array';
+ return false;
+ }
+
+ foreach ($value as $x => $child) {
+ if($required == false && is_null($child)) { // Allow null value to optional params
+ continue;
+ }
+
+ if(!$validator->isValid($child)) {
+ $this->message = 'Attribute "'.$key.'[\''.$x.'\']" has invalid type. '.$validator->getDescription();
+ return false;
+ }
+ }
+ }
+ else {
+ if($required == false && is_null($value)) { // Allow null value to optional params
+ continue;
+ }
+
+ if(!$validator->isValid($value)) {
+ $this->message = 'Attribute "'.$key.'" has invalid type. '.$validator->getDescription();
+ return false;
+ }
+ }
+
+ if($format) {
+ $validator = self::getFormat($format, $type);
+
+ if($array) { // Validate attribute type
+ if(!is_array($value)) {
+ $this->message = 'Attribute "'.$key.'" must be an array';
+ return false;
+ }
+
+ foreach ($value as $x => $child) {
+ if(!$validator->isValid($child)) {
+ $this->message = 'Attribute "'.$key.'[\''.$x.'\']" has invalid format. '.$validator->getDescription();
+ return false;
+ }
+ }
+ }
+ else {
+ if(!$validator->isValid($value)) {
+ $this->message = 'Attribute "'.$key.'" has invalid format. '.$validator->getDescription();
+ return false;
+ }
+ }
+ }
+
+ // TODO check for length / size
+ }
+
+ return true;
+ }
+
+ /**
+ * Is array
+ *
+ * Function will return true if object is array.
+ *
+ * @return bool
+ */
+ public function isArray(): bool
+ {
+ return false;
+ }
+
+ /**
+ * Get Type
+ *
+ * Returns validator type.
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return self::TYPE_ARRAY;
+ }
+}
diff --git a/src/Database/Validator/UID.php b/src/Database/Validator/UID.php
new file mode 100644
index 000000000..2742364ef
--- /dev/null
+++ b/src/Database/Validator/UID.php
@@ -0,0 +1,70 @@
+ 32) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Is array
+ *
+ * Function will return true if object is array.
+ *
+ * @return bool
+ */
+ public function isArray(): bool
+ {
+ return false;
+ }
+
+ /**
+ * Get Type
+ *
+ * Returns validator type.
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return self::TYPE_STRING;
+ }
+}
diff --git a/tests/Database/Adapter/CockroachTest.php b/tests/Database/Adapter/CockroachTest.php
new file mode 100644
index 000000000..3ca5c3ed3
--- /dev/null
+++ b/tests/Database/Adapter/CockroachTest.php
@@ -0,0 +1,46 @@
+ 'SET NAMES utf8mb4',
+// PDO::ATTR_TIMEOUT => 3, // Seconds
+// PDO::ATTR_PERSISTENT => true
+// ));
+
+// // Connection settings
+// $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // Return arrays
+// $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // Handle all errors with exceptions
+
+// $database = new Database(new Cockroach($pdo));
+// $database->setNamespace('myapp_'.uniqid());
+
+// return self::$database = $database;
+// }
+// }
\ No newline at end of file
diff --git a/tests/Database/Adapter/MariaDBTest.php b/tests/Database/Adapter/MariaDBTest.php
new file mode 100644
index 000000000..27dac6fb5
--- /dev/null
+++ b/tests/Database/Adapter/MariaDBTest.php
@@ -0,0 +1,52 @@
+ 'SET NAMES utf8mb4',
+ PDO::ATTR_TIMEOUT => 3, // Seconds
+ PDO::ATTR_PERSISTENT => true,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ ]);
+
+ $redis = new Redis();
+ $redis->connect('redis', 6379);
+ $redis->flushAll();
+ $cache = new Cache(new RedisAdapter($redis));
+
+ $database = new Database(new MariaDB($pdo), $cache);
+ $database->setNamespace('myapp_'.uniqid());
+
+ return self::$database = $database;
+ }
+}
\ No newline at end of file
diff --git a/tests/Database/Adapter/MongoDBTest.php b/tests/Database/Adapter/MongoDBTest.php
new file mode 100644
index 000000000..7664d3c4a
--- /dev/null
+++ b/tests/Database/Adapter/MongoDBTest.php
@@ -0,0 +1,39 @@
+ 'root',
+// 'password' => 'example',
+// ],
+// );
+
+// $database = new Database(new MongoDB($client));
+// $database->setNamespace('myapp_'.uniqid());
+
+// return self::$database = $database;
+// }
+// }
\ No newline at end of file
diff --git a/tests/Database/Adapter/MySQLTest.php b/tests/Database/Adapter/MySQLTest.php
new file mode 100644
index 000000000..4d3800d9f
--- /dev/null
+++ b/tests/Database/Adapter/MySQLTest.php
@@ -0,0 +1,54 @@
+ 'SET NAMES utf8mb4',
+ PDO::ATTR_TIMEOUT => 3, // Seconds
+ PDO::ATTR_PERSISTENT => true
+ ));
+
+ // Connection settings
+ $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // Return arrays
+ $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // Handle all errors with exceptions
+
+ $redis = new Redis();
+ $redis->connect('redis', 6379);
+ $redis->flushAll();
+ $cache = new Cache(new RedisAdapter($redis));
+
+ $database = new Database(new MySQL($pdo), $cache);
+ $database->setNamespace('myapp_'.uniqid());
+
+ return self::$database = $database;
+ }
+}
\ No newline at end of file
diff --git a/tests/Database/Adapter/PostgresTest.php b/tests/Database/Adapter/PostgresTest.php
new file mode 100644
index 000000000..9a152c7a5
--- /dev/null
+++ b/tests/Database/Adapter/PostgresTest.php
@@ -0,0 +1,44 @@
+ 'SET NAMES utf8mb4',
+// PDO::ATTR_TIMEOUT => 3, // Seconds
+// PDO::ATTR_PERSISTENT => true,
+// PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+// PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+// ]);
+
+// $database = new Database(new Postgres($pdo));
+// $database->setNamespace('myapp_'.uniqid());
+
+// return self::$database = $database;
+// }
+// }
\ No newline at end of file
diff --git a/tests/Database/Base.php b/tests/Database/Base.php
new file mode 100644
index 000000000..7d60bbe63
--- /dev/null
+++ b/tests/Database/Base.php
@@ -0,0 +1,949 @@
+assertEquals(false, static::getDatabase()->exists());
+ $this->assertEquals(true, static::getDatabase()->create());
+ $this->assertEquals(true, static::getDatabase()->exists());
+ $this->assertEquals(true, static::getDatabase()->delete());
+ $this->assertEquals(false, static::getDatabase()->exists());
+ $this->assertEquals(true, static::getDatabase()->create());
+ }
+
+ /**
+ * @depends testCreateExistsDelete
+ */
+ public function testCreateListDeleteCollection()
+ {
+ $this->assertInstanceOf('Utopia\Database\Document', static::getDatabase()->createCollection('actors'));
+
+ $this->assertCount(1, static::getDatabase()->listCollections());
+
+ $this->assertEquals(false, static::getDatabase()->getCollection('actors')->isEmpty());
+ $this->assertEquals(true, static::getDatabase()->deleteCollection('actors'));
+ $this->assertEquals(true, static::getDatabase()->getCollection('actors')->isEmpty());
+ }
+
+ public function testCreateDeleteAttribute()
+ {
+ static::getDatabase()->createCollection('attributes');
+
+ $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string1', Database::VAR_STRING, 128, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string2', Database::VAR_STRING, 16383+1, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string3', Database::VAR_STRING, 65535+1, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string4', Database::VAR_STRING, 16777215+1, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'integer', Database::VAR_INTEGER, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'float', Database::VAR_FLOAT, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'boolean', Database::VAR_BOOLEAN, 0, true));
+
+ $collection = static::getDatabase()->getCollection('attributes');
+ $this->assertCount(7, $collection->getAttribute('attributes'));
+
+ // Array
+ $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string_list', Database::VAR_STRING, 128, true, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'integer_list', Database::VAR_INTEGER, 0, true, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'float_list', Database::VAR_FLOAT, 0, true, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'boolean_list', Database::VAR_BOOLEAN, 0, true, true));
+
+ $collection = static::getDatabase()->getCollection('attributes');
+ $this->assertCount(11, $collection->getAttribute('attributes'));
+
+ // Delete
+ $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string1'));
+ $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string2'));
+ $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string3'));
+ $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string4'));
+ $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'integer'));
+ $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'float'));
+ $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'boolean'));
+
+ $collection = static::getDatabase()->getCollection('attributes');
+ $this->assertCount(4, $collection->getAttribute('attributes'));
+
+ // Delete Array
+ $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string_list'));
+ $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'integer_list'));
+ $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'float_list'));
+ $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'boolean_list'));
+
+ $collection = static::getDatabase()->getCollection('attributes');
+ $this->assertCount(0, $collection->getAttribute('attributes'));
+
+ static::getDatabase()->deleteCollection('attributes');
+ }
+
+ public function testAddRemoveAttribute()
+ {
+ static::getDatabase()->createCollection('attributesInQueue');
+
+ $this->assertEquals(true, static::getDatabase()->addAttributeInQueue('attributesInQueue', 'string1', Database::VAR_STRING, 128, true));
+ $this->assertEquals(true, static::getDatabase()->addAttributeInQueue('attributesInQueue', 'string2', Database::VAR_STRING, 16383+1, true));
+ $this->assertEquals(true, static::getDatabase()->addAttributeInQueue('attributesInQueue', 'string3', Database::VAR_STRING, 65535+1, true));
+ $this->assertEquals(true, static::getDatabase()->addAttributeInQueue('attributesInQueue', 'string4', Database::VAR_STRING, 16777215+1, true));
+ $this->assertEquals(true, static::getDatabase()->addAttributeInQueue('attributesInQueue', 'integer', Database::VAR_INTEGER, 0, true));
+ $this->assertEquals(true, static::getDatabase()->addAttributeInQueue('attributesInQueue', 'float', Database::VAR_FLOAT, 0, true));
+ $this->assertEquals(true, static::getDatabase()->addAttributeInQueue('attributesInQueue', 'boolean', Database::VAR_BOOLEAN, 0, true));
+
+ $collection = static::getDatabase()->getCollection('attributesInQueue');
+ $this->assertCount(7, $collection->getAttribute('attributesInQueue'));
+
+ // Array
+ $this->assertEquals(true, static::getDatabase()->addAttributeInQueue('attributesInQueue', 'string_list', Database::VAR_STRING, 128, true, true));
+ $this->assertEquals(true, static::getDatabase()->addAttributeInQueue('attributesInQueue', 'integer_list', Database::VAR_INTEGER, 0, true, true));
+ $this->assertEquals(true, static::getDatabase()->addAttributeInQueue('attributesInQueue', 'float_list', Database::VAR_FLOAT, 0, true, true));
+ $this->assertEquals(true, static::getDatabase()->addAttributeInQueue('attributesInQueue', 'boolean_list', Database::VAR_BOOLEAN, 0, true, true));
+
+ $collection = static::getDatabase()->getCollection('attributesInQueue');
+ $this->assertCount(11, $collection->getAttribute('attributesInQueue'));
+
+ // Delete
+ $this->assertEquals(true, static::getDatabase()->removeAttributeInQueue('attributesInQueue', 'string1'));
+ $this->assertEquals(true, static::getDatabase()->removeAttributeInQueue('attributesInQueue', 'string2'));
+ $this->assertEquals(true, static::getDatabase()->removeAttributeInQueue('attributesInQueue', 'string3'));
+ $this->assertEquals(true, static::getDatabase()->removeAttributeInQueue('attributesInQueue', 'string4'));
+ $this->assertEquals(true, static::getDatabase()->removeAttributeInQueue('attributesInQueue', 'integer'));
+ $this->assertEquals(true, static::getDatabase()->removeAttributeInQueue('attributesInQueue', 'float'));
+ $this->assertEquals(true, static::getDatabase()->removeAttributeInQueue('attributesInQueue', 'boolean'));
+
+ $collection = static::getDatabase()->getCollection('attributesInQueue');
+ $this->assertCount(4, $collection->getAttribute('attributesInQueue'));
+
+ // Delete Array
+ $this->assertEquals(true, static::getDatabase()->removeAttributeInQueue('attributesInQueue', 'string_list'));
+ $this->assertEquals(true, static::getDatabase()->removeAttributeInQueue('attributesInQueue', 'integer_list'));
+ $this->assertEquals(true, static::getDatabase()->removeAttributeInQueue('attributesInQueue', 'float_list'));
+ $this->assertEquals(true, static::getDatabase()->removeAttributeInQueue('attributesInQueue', 'boolean_list'));
+
+ $collection = static::getDatabase()->getCollection('attributesInQueue');
+ $this->assertCount(0, $collection->getAttribute('attributesInQueue'));
+
+ static::getDatabase()->deleteCollection('attributesInQueue');
+ }
+
+ public function testCreateDeleteIndex()
+ {
+ static::getDatabase()->createCollection('indexes');
+
+ $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'string', Database::VAR_STRING, 128, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'integer', Database::VAR_INTEGER, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'float', Database::VAR_FLOAT, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'boolean', Database::VAR_BOOLEAN, 0, true));
+
+ // Indexes
+ $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index1', Database::INDEX_KEY, ['string', 'integer'], [128], [Database::ORDER_ASC]));
+ $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index2', Database::INDEX_KEY, ['float', 'integer'], [], [Database::ORDER_ASC, Database::ORDER_DESC]));
+ $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index3', Database::INDEX_KEY, ['integer', 'boolean'], [], [Database::ORDER_ASC, Database::ORDER_DESC, Database::ORDER_DESC]));
+
+ $collection = static::getDatabase()->getCollection('indexes');
+ $this->assertCount(3, $collection->getAttribute('indexes'));
+
+ // Delete Indexes
+ $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index1'));
+ $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index2'));
+ $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index3'));
+
+ $collection = static::getDatabase()->getCollection('indexes');
+ $this->assertCount(0, $collection->getAttribute('indexes'));
+
+ static::getDatabase()->deleteCollection('indexes');
+ }
+
+ public function testAddRemoveIndexInQueue()
+ {
+ static::getDatabase()->createCollection('indexesInQueue');
+
+ $this->assertEquals(true, static::getDatabase()->createAttribute('indexesInQueue', 'string', Database::VAR_STRING, 128, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('indexesInQueue', 'integer', Database::VAR_INTEGER, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('indexesInQueue', 'float', Database::VAR_FLOAT, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('indexesInQueue', 'boolean', Database::VAR_BOOLEAN, 0, true));
+
+ // Indexes
+ $this->assertEquals(true, static::getDatabase()->addIndexInQueue('indexesInQueue', 'index1', Database::INDEX_KEY, ['string', 'integer'], [128], [Database::ORDER_ASC]));
+ $this->assertEquals(true, static::getDatabase()->addIndexInQueue('indexesInQueue', 'index2', Database::INDEX_KEY, ['float', 'integer'], [], [Database::ORDER_ASC, Database::ORDER_DESC]));
+ $this->assertEquals(true, static::getDatabase()->addIndexInQueue('indexesInQueue', 'index3', Database::INDEX_KEY, ['integer', 'boolean'], [], [Database::ORDER_ASC, Database::ORDER_DESC, Database::ORDER_DESC]));
+
+ $collection = static::getDatabase()->getCollection('indexesInQueue');
+ $this->assertCount(3, $collection->getAttribute('indexesInQueue'));
+
+ // Delete Indexes
+ $this->assertEquals(true, static::getDatabase()->removeIndexInQueue('indexesInQueue', 'index1'));
+ $this->assertEquals(true, static::getDatabase()->removeIndexInQueue('indexesInQueue', 'index2'));
+ $this->assertEquals(true, static::getDatabase()->removeIndexInQueue('indexesInQueue', 'index3'));
+
+ $collection = static::getDatabase()->getCollection('indexesInQueue');
+ $this->assertCount(0, $collection->getAttribute('indexesInQueue'));
+
+ static::getDatabase()->deleteCollection('indexesInQueue');
+ }
+
+ public function testCreateDocument()
+ {
+ static::getDatabase()->createCollection('documents');
+
+ $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'string', Database::VAR_STRING, 128, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'integer', Database::VAR_INTEGER, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'float', Database::VAR_FLOAT, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'boolean', Database::VAR_BOOLEAN, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'colors', Database::VAR_STRING, 32, true, true, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'empty', Database::VAR_STRING, 32, false, true, true));
+
+ $document = static::getDatabase()->createDocument('documents', new Document([
+ '$read' => ['*', 'user1', 'user2'],
+ '$write' => ['*', 'user1x', 'user2x'],
+ 'string' => 'text📝',
+ 'integer' => 5,
+ 'float' => 5.55,
+ 'boolean' => true,
+ 'colors' => ['pink', 'green', 'blue'],
+ 'empty' => [],
+ ]));
+
+ $this->assertNotEmpty(true, $document->getId());
+ $this->assertIsString($document->getAttribute('string'));
+ $this->assertEquals('text📝', $document->getAttribute('string')); // Also makes sure an emoji is working
+ $this->assertIsInt($document->getAttribute('integer'));
+ $this->assertEquals(5, $document->getAttribute('integer'));
+ $this->assertIsFloat($document->getAttribute('float'));
+ $this->assertEquals(5.55, $document->getAttribute('float'));
+ $this->assertIsBool($document->getAttribute('boolean'));
+ $this->assertEquals(true, $document->getAttribute('boolean'));
+ $this->assertIsArray($document->getAttribute('colors'));
+ $this->assertEquals(['pink', 'green', 'blue'], $document->getAttribute('colors'));
+ $this->assertEquals([], $document->getAttribute('empty'));
+
+ return $document;
+ }
+
+ /**
+ * @depends testCreateDocument
+ */
+ public function testGetDocument(Document $document)
+ {
+ $document = static::getDatabase()->getDocument('documents', $document->getId());
+
+ $this->assertNotEmpty(true, $document->getId());
+ $this->assertIsString($document->getAttribute('string'));
+ $this->assertEquals('text📝', $document->getAttribute('string'));
+ $this->assertIsInt($document->getAttribute('integer'));
+ $this->assertEquals(5, $document->getAttribute('integer'));
+ $this->assertIsFloat($document->getAttribute('float'));
+ $this->assertEquals(5.55, $document->getAttribute('float'));
+ $this->assertIsBool($document->getAttribute('boolean'));
+ $this->assertEquals(true, $document->getAttribute('boolean'));
+ $this->assertIsArray($document->getAttribute('colors'));
+ $this->assertEquals(['pink', 'green', 'blue'], $document->getAttribute('colors'));
+
+ return $document;
+ }
+
+ /**
+ * @depends testGetDocument
+ */
+ public function testUpdateDocument(Document $document)
+ {
+ $document
+ ->setAttribute('string', 'text📝 updated')
+ ->setAttribute('integer', 6)
+ ->setAttribute('float', 5.56)
+ ->setAttribute('boolean', false)
+ ->setAttribute('colors', 'red', Document::SET_TYPE_APPEND)
+ ;
+
+ $new = $this->getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document);
+
+ $this->assertNotEmpty(true, $new->getId());
+ $this->assertIsString($new->getAttribute('string'));
+ $this->assertEquals('text📝 updated', $new->getAttribute('string'));
+ $this->assertIsInt($new->getAttribute('integer'));
+ $this->assertEquals(6, $new->getAttribute('integer'));
+ $this->assertIsFloat($new->getAttribute('float'));
+ $this->assertEquals(5.56, $new->getAttribute('float'));
+ $this->assertIsBool($new->getAttribute('boolean'));
+ $this->assertEquals(false, $new->getAttribute('boolean'));
+ $this->assertIsArray($new->getAttribute('colors'));
+ $this->assertEquals(['pink', 'green', 'blue', 'red'], $new->getAttribute('colors'));
+
+ return $document;
+ }
+
+ /**
+ * @depends testUpdateDocument
+ */
+ public function testDeleteDocument(Document $document)
+ {
+ $result = $this->getDatabase()->deleteDocument($document->getCollection(), $document->getId());
+ $document = $this->getDatabase()->getDocument($document->getCollection(), $document->getId());
+
+ $this->assertEquals(true, $result);
+ $this->assertEquals(true, $document->isEmpty());
+ }
+
+ /**
+ * @depends testUpdateDocument
+ */
+ public function testFind(Document $document)
+ {
+ static::getDatabase()->createCollection('movies');
+
+ $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'name', Database::VAR_STRING, 128, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'director', Database::VAR_STRING, 128, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'year', Database::VAR_INTEGER, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'price', Database::VAR_FLOAT, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'active', Database::VAR_BOOLEAN, 0, true));
+ $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'generes', Database::VAR_STRING, 32, true, true, true));
+
+ static::getDatabase()->createDocument('movies', new Document([
+ '$read' => ['*', 'user1', 'user2'],
+ '$write' => ['*', 'user1x', 'user2x'],
+ 'name' => 'Frozen',
+ 'director' => 'Chris Buck & Jennifer Lee',
+ 'year' => 2013,
+ 'price' => 39.50,
+ 'active' => true,
+ 'generes' => ['animation', 'kids'],
+ ]));
+
+ static::getDatabase()->createDocument('movies', new Document([
+ '$read' => ['*', 'user1', 'user2'],
+ '$write' => ['*', 'user1x', 'user2x'],
+ 'name' => 'Frozen II',
+ 'director' => 'Chris Buck & Jennifer Lee',
+ 'year' => 2019,
+ 'price' => 39.50,
+ 'active' => true,
+ 'generes' => ['animation', 'kids'],
+ ]));
+
+ static::getDatabase()->createDocument('movies', new Document([
+ '$read' => ['*', 'user1', 'user2'],
+ '$write' => ['*', 'user1x', 'user2x'],
+ 'name' => 'Captain America: The First Avenger',
+ 'director' => 'Joe Johnston',
+ 'year' => 2011,
+ 'price' => 25.94,
+ 'active' => true,
+ 'generes' => ['science fiction', 'action', 'comics'],
+ ]));
+
+ static::getDatabase()->createDocument('movies', new Document([
+ '$read' => ['*', 'user1', 'user2'],
+ '$write' => ['*', 'user1x', 'user2x'],
+ 'name' => 'Captain Marvel',
+ 'director' => 'Anna Boden & Ryan Fleck',
+ 'year' => 2019,
+ 'price' => 25.99,
+ 'active' => true,
+ 'generes' => ['science fiction', 'action', 'comics'],
+ ]));
+
+ static::getDatabase()->createDocument('movies', new Document([
+ '$read' => ['*', 'user1', 'user2'],
+ '$write' => ['*', 'user1x', 'user2x'],
+ 'name' => 'Work in Progress',
+ 'director' => 'TBD',
+ 'year' => 2025,
+ 'price' => 0.0,
+ 'active' => false,
+ 'generes' => [],
+ ]));
+
+ static::getDatabase()->createDocument('movies', new Document([
+ '$read' => ['userx'],
+ '$write' => ['*', 'user1x', 'user2x'],
+ 'name' => 'Work in Progress 2',
+ 'director' => 'TBD',
+ 'year' => 2026,
+ 'price' => 0.0,
+ 'active' => false,
+ 'generes' => [],
+ ]));
+
+ /**
+ * Check Basic
+ */
+ $documents = static::getDatabase()->find('movies');
+
+ $this->assertEquals(5, count($documents));
+ $this->assertNotEmpty($documents[0]->getId());
+ $this->assertEquals('movies', $documents[0]->getCollection());
+ $this->assertEquals(['*', 'user1', 'user2'], $documents[0]->getRead());
+ $this->assertEquals(['*', 'user1x', 'user2x'], $documents[0]->getWrite());
+ $this->assertEquals('Frozen', $documents[0]->getAttribute('name'));
+ $this->assertEquals('Chris Buck & Jennifer Lee', $documents[0]->getAttribute('director'));
+ $this->assertIsString($documents[0]->getAttribute('director'));
+ $this->assertEquals(2013, $documents[0]->getAttribute('year'));
+ $this->assertIsInt($documents[0]->getAttribute('year'));
+ $this->assertEquals(39.50, $documents[0]->getAttribute('price'));
+ $this->assertIsFloat($documents[0]->getAttribute('price'));
+ $this->assertEquals(true, $documents[0]->getAttribute('active'));
+ $this->assertIsBool($documents[0]->getAttribute('active'));
+ $this->assertEquals(['animation', 'kids'], $documents[0]->getAttribute('generes'));
+ $this->assertIsArray($documents[0]->getAttribute('generes'));
+
+ /**
+ * Check Permissions
+ */
+ Authorization::setRole('userx');
+
+ $documents = static::getDatabase()->find('movies');
+
+ $this->assertEquals(6, count($documents));
+
+ /**
+ * Check an Integer condition
+ */
+ $documents = static::getDatabase()->find('movies', [
+ new Query('year', Query::TYPE_EQUAL, [2019]),
+ ]);
+
+ $this->assertEquals(2, count($documents));
+ $this->assertEquals('Frozen II', $documents[0]['name']);
+ $this->assertEquals('Captain Marvel', $documents[1]['name']);
+
+ /**
+ * Boolean condition
+ */
+ $documents = static::getDatabase()->find('movies', [
+ new Query('active', Query::TYPE_EQUAL, [true]),
+ ]);
+
+ $this->assertEquals(4, count($documents));
+
+ /**
+ * String condition
+ */
+ $documents = static::getDatabase()->find('movies', [
+ new Query('director', Query::TYPE_EQUAL, ['TBD']),
+ ]);
+
+ $this->assertEquals(2, count($documents));
+
+ /**
+ * Float condition
+ */
+ $documents = static::getDatabase()->find('movies', [
+ new Query('price', Query::TYPE_LESSER, [26.00]),
+ new Query('price', Query::TYPE_GREATER, [25.98]),
+ ]);
+
+ $this->assertEquals(1, count($documents));
+ $this->assertEquals('Captain Marvel', $documents[0]['name']);
+
+ /**
+ * Multiple conditions
+ */
+ $documents = static::getDatabase()->find('movies', [
+ new Query('director', Query::TYPE_EQUAL, ['TBD']),
+ new Query('year', Query::TYPE_EQUAL, [2026]),
+ ]);
+
+ $this->assertEquals(1, count($documents));
+
+ /**
+ * Multiple conditions and OR values
+ */
+ $documents = static::getDatabase()->find('movies', [
+ new Query('name', Query::TYPE_EQUAL, ['Frozen II', 'Captain Marvel']),
+ ]);
+
+ $this->assertEquals(2, count($documents));
+ $this->assertEquals('Frozen II', $documents[0]['name']);
+ $this->assertEquals('Captain Marvel', $documents[1]['name']);
+
+ /**
+ * ORDER BY
+ */
+ $documents = static::getDatabase()->find('movies', [], 25, 0, ['price'], [Database::ORDER_DESC]);
+
+ $this->assertEquals(6, count($documents));
+ $this->assertEquals('Frozen', $documents[0]['name']);
+ $this->assertEquals('Frozen II', $documents[1]['name']);
+ $this->assertEquals('Captain Marvel', $documents[2]['name']);
+ $this->assertEquals('Captain America: The First Avenger', $documents[3]['name']);
+ $this->assertEquals('Work in Progress', $documents[4]['name']);
+ $this->assertEquals('Work in Progress 2', $documents[5]['name']);
+
+ /**
+ * ORDER BY - Multiple attributes
+ */
+ $documents = static::getDatabase()->find('movies', [], 25, 0, ['price', 'name'], [Database::ORDER_DESC, Database::ORDER_DESC]);
+
+ $this->assertEquals(6, count($documents));
+ $this->assertEquals('Frozen II', $documents[0]['name']);
+ $this->assertEquals('Frozen', $documents[1]['name']);
+ $this->assertEquals('Captain Marvel', $documents[2]['name']);
+ $this->assertEquals('Captain America: The First Avenger', $documents[3]['name']);
+ $this->assertEquals('Work in Progress 2', $documents[4]['name']);
+ $this->assertEquals('Work in Progress', $documents[5]['name']);
+
+ /**
+ * Limit
+ */
+ $documents = static::getDatabase()->find('movies', [], 4, 0);
+
+ $this->assertEquals(4, count($documents));
+ $this->assertEquals('Frozen', $documents[0]['name']);
+ $this->assertEquals('Frozen II', $documents[1]['name']);
+ $this->assertEquals('Captain America: The First Avenger', $documents[2]['name']);
+ $this->assertEquals('Captain Marvel', $documents[3]['name']);
+
+ /**
+ * Limit + Offset
+ */
+ $documents = static::getDatabase()->find('movies', [], 4, 2);
+
+ $this->assertEquals(4, count($documents));
+ $this->assertEquals('Captain America: The First Avenger', $documents[0]['name']);
+ $this->assertEquals('Captain Marvel', $documents[1]['name']);
+ $this->assertEquals('Work in Progress', $documents[2]['name']);
+ $this->assertEquals('Work in Progress 2', $documents[3]['name']);
+ }
+
+ /**
+ * @depends testFind
+ */
+ public function testFindFirst()
+ {
+ $document = static::getDatabase()->findFirst('movies', [], 4, 2);
+ $this->assertEquals('Captain America: The First Avenger', $document['name']);
+
+ $document = static::getDatabase()->findFirst('movies', [], 4, 10);
+ $this->assertEquals(false, $document);
+ }
+
+ /**
+ * @depends testFind
+ */
+ public function testFindLast()
+ {
+ $document = static::getDatabase()->findLast('movies', [], 4, 2);
+ $this->assertEquals('Work in Progress 2', $document['name']);
+
+ $document = static::getDatabase()->findLast('movies', [], 4, 10);
+ $this->assertEquals(false, $document);
+ }
+
+ /**
+ * @depends testFind
+ */
+ public function testCount()
+ {
+ $count = static::getDatabase()->count('movies');
+ $this->assertEquals(6, $count);
+
+ $count = static::getDatabase()->count('movies', [new Query('year', Query::TYPE_EQUAL, [2019]),]);
+ $this->assertEquals(2, $count);
+
+ Authorization::unsetRole('userx');
+ $count = static::getDatabase()->count('movies');
+ $this->assertEquals(5, $count);
+
+ Authorization::disable();
+ $count = static::getDatabase()->count('movies');
+ $this->assertEquals(6, $count);
+ Authorization::reset();
+
+ Authorization::disable();
+ $count = static::getDatabase()->count('movies', [], 3);
+ $this->assertEquals(3, $count);
+ Authorization::reset();
+ }
+
+ public function testEncodeDecode()
+ {
+ $collection = new Document([
+ '$collection' => Database::COLLECTIONS,
+ '$id' => 'users',
+ 'name' => 'Users',
+ 'attributes' => [
+ [
+ '$id' => 'name',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 256,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'email',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 1024,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'status',
+ 'type' => Database::VAR_INTEGER,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'password',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'passwordUpdate',
+ 'type' => Database::VAR_INTEGER,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'registration',
+ 'type' => Database::VAR_INTEGER,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'emailVerification',
+ 'type' => Database::VAR_BOOLEAN,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'reset',
+ 'type' => Database::VAR_BOOLEAN,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'prefs',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => ['json']
+ ],
+ [
+ '$id' => 'sessions',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => ['json'],
+ ],
+ [
+ '$id' => 'tokens',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => ['json'],
+ ],
+ [
+ '$id' => 'memberships',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => false,
+ 'filters' => ['json'],
+ ],
+ [
+ '$id' => 'roles',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 128,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => true,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'tags',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 128,
+ 'signed' => true,
+ 'required' => false,
+ 'array' => true,
+ 'filters' => ['json'],
+ ],
+ ],
+ 'indexes' => [
+ [
+ '$id' => '_key_email',
+ 'type' => Database::INDEX_UNIQUE,
+ 'attributes' => ['email'],
+ 'lengths' => [1024],
+ 'orders' => [Database::ORDER_ASC],
+ ]
+ ],
+ ]);
+
+ $document = new Document([
+ '$id' => '608fdbe51361a',
+ '$read' => ['*'],
+ '$write' => ['user:608fdbe51361a'],
+ 'email' => 'test@example.com',
+ 'emailVerification' => false,
+ 'status' => 1,
+ 'password' => 'randomhash',
+ 'passwordUpdate' => 1234,
+ 'registration' => 1234,
+ 'reset' => false,
+ 'name' => 'My Name',
+ 'prefs' => new stdClass,
+ 'sessions' => [],
+ 'tokens' => [],
+ 'memberships' => [],
+ 'roles' => [
+ 'admin',
+ 'developer',
+ 'tester',
+ ],
+ 'tags' => [
+ ['$id' => '1', 'label' => 'x'],
+ ['$id' => '2', 'label' => 'y'],
+ ['$id' => '3', 'label' => 'z'],
+ ],
+ ]);
+
+ $result = static::getDatabase()->encode($collection, $document);
+
+ $this->assertEquals('608fdbe51361a', $result->getAttribute('$id'));
+ $this->assertEquals(['*'], $result->getAttribute('$read'));
+ $this->assertEquals(['user:608fdbe51361a'], $result->getAttribute('$write'));
+ $this->assertEquals('test@example.com', $result->getAttribute('email'));
+ $this->assertEquals(false, $result->getAttribute('emailVerification'));
+ $this->assertEquals(1, $result->getAttribute('status'));
+ $this->assertEquals('randomhash', $result->getAttribute('password'));
+ $this->assertEquals(1234, $result->getAttribute('passwordUpdate'));
+ $this->assertEquals(1234, $result->getAttribute('registration'));
+ $this->assertEquals(false, $result->getAttribute('reset'));
+ $this->assertEquals('My Name', $result->getAttribute('name'));
+ $this->assertEquals('{}', $result->getAttribute('prefs'));
+ $this->assertEquals('[]', $result->getAttribute('sessions'));
+ $this->assertEquals('[]', $result->getAttribute('tokens'));
+ $this->assertEquals('[]', $result->getAttribute('memberships'));
+ $this->assertEquals(['admin','developer','tester',], $result->getAttribute('roles'));
+ $this->assertEquals(['{"$id":"1","label":"x"}','{"$id":"2","label":"y"}','{"$id":"3","label":"z"}',], $result->getAttribute('tags'));
+
+ $result = static::getDatabase()->decode($collection, $document);
+
+ $this->assertEquals('608fdbe51361a', $result->getAttribute('$id'));
+ $this->assertEquals(['*'], $result->getAttribute('$read'));
+ $this->assertEquals(['user:608fdbe51361a'], $result->getAttribute('$write'));
+ $this->assertEquals('test@example.com', $result->getAttribute('email'));
+ $this->assertEquals(false, $result->getAttribute('emailVerification'));
+ $this->assertEquals(1, $result->getAttribute('status'));
+ $this->assertEquals('randomhash', $result->getAttribute('password'));
+ $this->assertEquals(1234, $result->getAttribute('passwordUpdate'));
+ $this->assertEquals(1234, $result->getAttribute('registration'));
+ $this->assertEquals(false, $result->getAttribute('reset'));
+ $this->assertEquals('My Name', $result->getAttribute('name'));
+ $this->assertEquals([], $result->getAttribute('prefs'));
+ $this->assertEquals([], $result->getAttribute('sessions'));
+ $this->assertEquals([], $result->getAttribute('tokens'));
+ $this->assertEquals([], $result->getAttribute('memberships'));
+ $this->assertEquals(['admin','developer','tester',], $result->getAttribute('roles'));
+ $this->assertEquals([
+ ['$id' => '1', 'label' => 'x'],
+ ['$id' => '2', 'label' => 'y'],
+ ['$id' => '3', 'label' => 'z'],
+ ], $result->getAttribute('tags'));
+ }
+
+ /**
+ * @depends testCreateDocument
+ */
+ public function testReadPermissionsSuccess(Document $document)
+ {
+ $document = static::getDatabase()->createDocument('documents', new Document([
+ '$read' => ['*'],
+ '$write' => ['*'],
+ 'string' => 'text📝',
+ 'integer' => 5,
+ 'float' => 5.55,
+ 'boolean' => true,
+ 'colors' => ['pink', 'green', 'blue'],
+ ]));
+
+ $this->assertEquals(false, $document->isEmpty());
+
+ Authorization::cleanRoles();
+
+ $document = static::getDatabase()->getDocument($document->getCollection(), $document->getId());
+
+ $this->assertEquals(true, $document->isEmpty());
+
+ Authorization::setRole('*');
+
+ return $document;
+ }
+
+ /**
+ * @depends testCreateDocument
+ */
+ public function testReadPermissionsFailure(Document $document)
+ {
+ $this->expectException(ExceptionAuthorization::class);
+
+ $document = static::getDatabase()->createDocument('documents', new Document([
+ '$read' => ['user1'],
+ '$write' => ['user1'],
+ 'string' => 'text📝',
+ 'integer' => 5,
+ 'float' => 5.55,
+ 'boolean' => true,
+ 'colors' => ['pink', 'green', 'blue'],
+ ]));
+
+ return $document;
+ }
+
+ /**
+ * @depends testCreateDocument
+ */
+ public function testWritePermissionsSuccess(Document $document)
+ {
+ $document = static::getDatabase()->createDocument('documents', new Document([
+ '$read' => ['*'],
+ '$write' => ['*'],
+ 'string' => 'text📝',
+ 'integer' => 5,
+ 'float' => 5.55,
+ 'boolean' => true,
+ 'colors' => ['pink', 'green', 'blue'],
+ ]));
+
+ $this->assertEquals(false, $document->isEmpty());
+
+ return $document;
+ }
+
+ /**
+ * @depends testCreateDocument
+ */
+ public function testWritePermissionsFailure(Document $document)
+ {
+ $this->expectException(ExceptionAuthorization::class);
+
+ Authorization::cleanRoles();
+
+ $document = static::getDatabase()->createDocument('documents', new Document([
+ '$read' => ['*'],
+ '$write' => ['*'],
+ 'string' => 'text📝',
+ 'integer' => 5,
+ 'float' => 5.55,
+ 'boolean' => true,
+ 'colors' => ['pink', 'green', 'blue'],
+ ]));
+
+ return $document;
+ }
+
+ /**
+ * @depends testCreateDocument
+ */
+ public function testWritePermissionsUpdateFailure(Document $document)
+ {
+ $this->expectException(ExceptionAuthorization::class);
+
+ $document = static::getDatabase()->createDocument('documents', new Document([
+ '$read' => ['*'],
+ '$write' => ['*'],
+ 'string' => 'text📝',
+ 'integer' => 5,
+ 'float' => 5.55,
+ 'boolean' => true,
+ 'colors' => ['pink', 'green', 'blue'],
+ ]));
+
+ Authorization::cleanRoles();
+
+ $document = static::getDatabase()->updateDocument('documents', $document->getId(), new Document([
+ '$id' => $document->getId(),
+ '$read' => ['*'],
+ '$write' => ['*'],
+ 'string' => 'text📝',
+ 'integer' => 5,
+ 'float' => 5.55,
+ 'boolean' => true,
+ 'colors' => ['pink', 'green', 'blue'],
+ ]));
+
+ return $document;
+ }
+
+ /**
+ * @depends testGetDocument
+ */
+ public function testExceptionDuplicate(Document $document)
+ {
+ $this->expectException(Duplicate::class);
+
+ $document->setAttribute('$id', 'duplicated');
+
+ static::getDatabase()->createDocument($document->getCollection(), $document);
+ static::getDatabase()->createDocument($document->getCollection(), $document);
+
+ return $document;
+ }
+}
\ No newline at end of file
diff --git a/tests/Database/DocumentTest.php b/tests/Database/DocumentTest.php
new file mode 100644
index 000000000..2a873e7e5
--- /dev/null
+++ b/tests/Database/DocumentTest.php
@@ -0,0 +1,173 @@
+id = uniqid();
+
+ $this->collection = uniqid();
+
+ $this->document = new Document([
+ '$id' => $this->id,
+ '$collection' => $this->collection,
+ '$read' => ['user:123', 'team:123'],
+ '$write' => ['*'],
+ 'title' => 'This is a test.',
+ 'list' => [
+ 'one'
+ ],
+ 'children' => [
+ new Document(['name' => 'x']),
+ new Document(['name' => 'y']),
+ new Document(['name' => 'z']),
+ ]
+ ]);
+
+ $this->empty = new Document();
+ }
+
+ public function tearDown(): void
+ {
+ }
+
+ public function testId()
+ {
+ $this->assertEquals($this->id, $this->document->getId());
+ $this->assertEquals(null, $this->empty->getId());
+ }
+
+ public function testCollection()
+ {
+ $this->assertEquals($this->collection, $this->document->getCollection());
+ $this->assertEquals(null, $this->empty->getCollection());
+ }
+
+ public function testPermissions()
+ {
+ $this->assertEquals(['user:123', 'team:123'], $this->document->getRead());
+ $this->assertEquals(['*'], $this->document->getWrite());
+ }
+
+ public function testGetAttributes()
+ {
+ $this->assertEquals([
+ 'title' => 'This is a test.',
+ 'list' => [
+ 'one'
+ ],
+ 'children' => [
+ new Document(['name' => 'x']),
+ new Document(['name' => 'y']),
+ new Document(['name' => 'z']),
+ ]
+ ], $this->document->getAttributes());
+ }
+
+ public function testGetAttribute()
+ {
+ $this->assertEquals('This is a test.', $this->document->getAttribute('title', ''));
+ $this->assertEquals('', $this->document->getAttribute('titlex', ''));
+ }
+
+ public function testSetAttribute()
+ {
+ $this->assertEquals('This is a test.', $this->document->getAttribute('title', ''));
+ $this->assertEquals(['one'], $this->document->getAttribute('list', []));
+ $this->assertEquals('', $this->document->getAttribute('titlex', ''));
+
+ $this->document->setAttribute('title', 'New title');
+
+ $this->assertEquals('New title', $this->document->getAttribute('title', ''));
+ $this->assertEquals('', $this->document->getAttribute('titlex', ''));
+
+ $this->document->setAttribute('list', 'two', Document::SET_TYPE_APPEND);
+ $this->assertEquals(['one', 'two'], $this->document->getAttribute('list', []));
+
+ $this->document->setAttribute('list', 'zero', Document::SET_TYPE_PREPEND);
+ $this->assertEquals(['zero', 'one', 'two'], $this->document->getAttribute('list', []));
+
+ $this->document->setAttribute('list', ['one'], Document::SET_TYPE_ASSIGN);
+ $this->assertEquals(['one'], $this->document->getAttribute('list', []));
+ }
+
+ public function testRemoveAttribute()
+ {
+ $this->document->removeAttribute('list');
+ $this->assertEquals([], $this->document->getAttribute('list', []));
+ }
+
+ public function testSearch()
+ {
+ $this->assertEquals(null, $this->document->search('find', 'one'));
+
+ $this->document->setAttribute('findString', 'demo');
+ $this->assertEquals($this->document, $this->document->search('findString', 'demo'));
+
+ $this->document->setAttribute('findArray', ['demo']);
+ $this->assertEquals(null, $this->document->search('findArray', 'demo'));
+ $this->assertEquals($this->document, $this->document->search('findArray', ['demo']));
+
+ $this->assertEquals($this->document->getAttribute('children')[0], $this->document->search('name', 'x', $this->document->getAttribute('children')));
+ $this->assertEquals($this->document->getAttribute('children')[2], $this->document->search('name', 'z', $this->document->getAttribute('children')));
+ $this->assertEquals(null, $this->document->search('name', 'v', $this->document->getAttribute('children')));
+ }
+
+ public function testIsEmpty()
+ {
+ $this->assertEquals(false, $this->document->isEmpty());
+ $this->assertEquals(true, $this->empty->isEmpty());
+ }
+
+ public function testIsSet()
+ {
+ $this->assertEquals(false, $this->document->isSet('titlex'));
+ $this->assertEquals(false, $this->empty->isSet('titlex'));
+ $this->assertEquals(true, $this->document->isSet('title'));
+ }
+
+ public function testGetArrayCopy()
+ {
+ $this->assertEquals([
+ '$id' => $this->id,
+ '$collection' => $this->collection,
+ '$read' => ['user:123', 'team:123'],
+ '$write' => ['*'],
+ 'title' => 'This is a test.',
+ 'list' => [
+ 'one'
+ ],
+ 'children' => [
+ ['name' => 'x'],
+ ['name' => 'y'],
+ ['name' => 'z'],
+ ]
+ ], $this->document->getArrayCopy());
+ $this->assertEquals([], $this->empty->getArrayCopy());
+ }
+}
\ No newline at end of file
diff --git a/tests/Database/Format.php b/tests/Database/Format.php
new file mode 100644
index 000000000..7b0899ba8
--- /dev/null
+++ b/tests/Database/Format.php
@@ -0,0 +1,45 @@
+assertEquals('title', $query->getAttribute());
+ $this->assertEquals('equal', $query->getOperator());
+ $this->assertContains('Iron Man', $query->getValues());
+ }
+
+ public function testParse()
+ {
+ $query = Query::parse('title.equal("Iron Man")');
+
+ $this->assertEquals('title', $query->getAttribute());
+ $this->assertEquals('equal', $query->getOperator());
+ $this->assertContains('Iron Man', $query->getValues());
+
+ $query = Query::parse('year.lesser(2001)');
+
+ $this->assertEquals('year', $query->getAttribute());
+ $this->assertEquals('lesser', $query->getOperator());
+ $this->assertContains(2001, $query->getValues());
+
+ $query = Query::parse('published.equal(true)');
+
+ $this->assertEquals('published', $query->getAttribute());
+ $this->assertEquals('equal', $query->getOperator());
+ $this->assertContains(true, $query->getValues());
+
+ $query = Query::parse('published.equal(false)');
+
+ $this->assertEquals('published', $query->getAttribute());
+ $this->assertEquals('equal', $query->getOperator());
+ $this->assertContains(false, $query->getValues());
+
+ $query = Query::parse('actors.notContains( " Johnny Depp ", " Brad Pitt" , "Al Pacino")');
+
+ $this->assertEquals('actors', $query->getAttribute());
+ $this->assertEquals('notContains', $query->getOperator());
+ $this->assertContains(' Johnny Depp ', $query->getValues());
+ $this->assertContains(' Brad Pitt', $query->getValues());
+ $this->assertContains('Al Pacino', $query->getValues());
+
+ $query = Query::parse('actors.equal("Brad Pitt", "Johnny Depp")');
+
+ $this->assertEquals('actors', $query->getAttribute());
+ $this->assertEquals('equal', $query->getOperator());
+ $this->assertContains('Brad Pitt', $query->getValues());
+ $this->assertContains('Johnny Depp', $query->getValues());
+
+ $query = Query::parse('writers.contains("Tim O\'Reilly")');
+
+ $this->assertEquals('writers', $query->getAttribute());
+ $this->assertEquals('contains', $query->getOperator());
+ $this->assertContains("Tim O'Reilly", $query->getValues());
+
+ $query = Query::parse('score.greater(8.5)');
+
+ $this->assertEquals('score', $query->getAttribute());
+ $this->assertEquals('greater', $query->getOperator());
+ $this->assertContains(8.5, $query->getValues());
+
+ $query = Query::parse('director.notEqual("null")');
+
+ $this->assertEquals('director', $query->getAttribute());
+ $this->assertEquals('notEqual', $query->getOperator());
+ $this->assertContains('null', $query->getValues());
+
+ $query = Query::parse('director.notEqual(null)');
+
+ $this->assertEquals('director', $query->getAttribute());
+ $this->assertEquals('notEqual', $query->getOperator());
+ $this->assertContains(null, $query->getValues());
+ }
+
+ public function testGetAttribute()
+ {
+ $query = Query::parse('title.equal("Iron Man")');
+
+ $this->assertEquals('title', $query->getAttribute());
+ }
+
+ public function testGetOperator()
+ {
+ $query = Query::parse('title.equal("Iron Man")');
+
+ $this->assertEquals('equal', $query->getOperator());
+ }
+
+ public function testGetValue()
+ {
+ $query = Query::parse('title.equal("Iron Man")');
+
+ $this->assertContains('Iron Man', $query->getValues());
+ }
+
+ public function testGetQuery()
+ {
+ $query = Query::parse('title.equal("Iron Man")')->getQuery();
+
+ $this->assertEquals('title', $query['attribute']);
+ $this->assertEquals('equal', $query['operator']);
+ $this->assertContains('Iron Man', $query['values']);
+ }
+
+}
\ No newline at end of file
diff --git a/tests/Database/Validator/AuthorizationTest.php b/tests/Database/Validator/AuthorizationTest.php
new file mode 100644
index 000000000..ca7ee547b
--- /dev/null
+++ b/tests/Database/Validator/AuthorizationTest.php
@@ -0,0 +1,91 @@
+document = new Document([
+ '$id' => uniqid(),
+ '$collection' => uniqid(),
+ '$read' => ['user:123', 'team:123'],
+ '$write' => ['*'],
+ ]);
+ $this->object = new Authorization($this->document, 'read');
+ }
+
+ public function tearDown(): void
+ {
+ }
+
+ public function testValues()
+ {
+ $this->assertEquals($this->object->isValid($this->document->getRead()), false);
+ $this->assertEquals($this->object->isValid(''), false);
+ $this->assertEquals($this->object->isValid([]), false);
+ $this->assertEquals($this->object->getDescription(), 'No permissions provided for action \'read\'');
+
+ Authorization::setRole('user:456');
+ Authorization::setRole('user:123');
+
+ $this->assertEquals(Authorization::isRole('user:456'), true);
+ $this->assertEquals(Authorization::isRole('user:457'), false);
+ $this->assertEquals(Authorization::isRole(''), false);
+ $this->assertEquals(Authorization::isRole('*'), true);
+
+ $this->assertEquals($this->object->isValid($this->document->getRead()), true);
+
+ Authorization::cleanRoles();
+
+ $this->assertEquals($this->object->isValid($this->document->getRead()), false);
+
+ Authorization::setRole('team:123');
+
+ $this->assertEquals($this->object->isValid($this->document->getRead()), true);
+
+ Authorization::cleanRoles();
+ Authorization::disable();
+
+ $this->assertEquals($this->object->isValid($this->document->getRead()), true);
+
+ Authorization::reset();
+
+ $this->assertEquals($this->object->isValid($this->document->getRead()), false);
+
+ Authorization::setDefaultStatus(false);
+ Authorization::disable();
+
+ $this->assertEquals($this->object->isValid($this->document->getRead()), true);
+
+ Authorization::reset();
+
+ $this->assertEquals($this->object->isValid($this->document->getRead()), true);
+
+ Authorization::enable();
+
+ $this->assertEquals($this->object->isValid($this->document->getRead()), false);
+
+ Authorization::setRole('textX');
+
+ $this->assertContains('textX', Authorization::getRoles());
+
+ Authorization::unsetRole('textX');
+
+ $this->assertNotContains('textX', Authorization::getRoles());
+ }
+}
\ No newline at end of file
diff --git a/tests/Database/Validator/KeyTest.php b/tests/Database/Validator/KeyTest.php
new file mode 100644
index 000000000..6b6e21dcd
--- /dev/null
+++ b/tests/Database/Validator/KeyTest.php
@@ -0,0 +1,39 @@
+object = new Key();
+ }
+
+ public function tearDown(): void
+ {
+ }
+
+ public function testValues()
+ {
+ $this->assertEquals($this->object->isValid('dasda asdasd'), false);
+ $this->assertEquals($this->object->isValid('asdasdasdas'), true);
+ $this->assertEquals($this->object->isValid('_asdasdasdas'), false);
+ $this->assertEquals($this->object->isValid('asd"asdasdas'), false);
+ $this->assertEquals($this->object->isValid('asd\'asdasdas'), false);
+ $this->assertEquals($this->object->isValid('as$$5dasdasdas'), false);
+ $this->assertEquals($this->object->isValid(false), false);
+ $this->assertEquals($this->object->isValid(null), false);
+ $this->assertEquals($this->object->isValid('socialAccountForYoutubeSubscribers'), false);
+ $this->assertEquals($this->object->isValid('socialAccountForYoutubeSubscriber'), false);
+ $this->assertEquals($this->object->isValid('socialAccountForYoutubeSubscribe'), true);
+ $this->assertEquals($this->object->isValid('socialAccountForYoutubeSubscrib'), true);
+ }
+}
diff --git a/tests/Database/Validator/PermissionsTest.php b/tests/Database/Validator/PermissionsTest.php
new file mode 100644
index 000000000..0fb679bcf
--- /dev/null
+++ b/tests/Database/Validator/PermissionsTest.php
@@ -0,0 +1,44 @@
+ uniqid(),
+ '$collection' => uniqid(),
+ '$read' => ['user:123', 'team:123'],
+ '$write' => ['*'],
+ ]);
+
+ $this->assertEquals($object->isValid($document->getRead()), true);
+
+ $document = new Document([
+ '$id' => uniqid(),
+ '$collection' => uniqid(),
+ '$read' => ['user:123', 'team:123'],
+ ]);
+
+ $this->assertEquals($object->isValid($document->getRead()), true);
+ $this->assertEquals($object->isValid('sting'), false);
+
+ }
+}
\ No newline at end of file
diff --git a/tests/Database/Validator/QueryValidatorTest.php b/tests/Database/Validator/QueryValidatorTest.php
new file mode 100644
index 000000000..4ad863158
--- /dev/null
+++ b/tests/Database/Validator/QueryValidatorTest.php
@@ -0,0 +1,120 @@
+ 'title',
+ 'type' => Database::VAR_STRING,
+ 'size' => 256,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'description',
+ 'type' => Database::VAR_STRING,
+ 'size' => 1000000,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'rating',
+ 'type' => Database::VAR_INTEGER,
+ 'size' => 5,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'price',
+ 'type' => Database::VAR_FLOAT,
+ 'size' => 5,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'published',
+ 'type' => Database::VAR_BOOLEAN,
+ 'size' => 5,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'tags',
+ 'type' => Database::VAR_STRING,
+ 'size' => 55,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => true,
+ 'filters' => [],
+ ],
+ ];
+
+ public function setUp(): void
+ {
+ }
+
+ public function tearDown(): void
+ {
+ }
+
+ public function testQuery()
+ {
+ $validator = new QueryValidator($this->schema);
+
+ $this->assertEquals(true, $validator->isValid(Query::parse('title.notEqual("Iron Man", "Ant Man")')));
+ $this->assertEquals(true, $validator->isValid(Query::parse('description.equal("Best movie ever")')));
+ $this->assertEquals(true, $validator->isValid(Query::parse('rating.greater(4)')));
+
+ $this->assertEquals(true, $validator->isValid(Query::parse('price.lesserEqual(6.50)')));
+ }
+
+ public function testInvalidOperator()
+ {
+ $validator = new QueryValidator($this->schema);
+
+ $response = $validator->isValid(Query::parse('title.eqqual("Iron Man")'));
+
+ $this->assertEquals(false, $response);
+ $this->assertEquals('Query operator invalid: eqqual', $validator->getDescription());
+ }
+
+ public function testAttributeNotFound()
+ {
+ $validator = new QueryValidator($this->schema);
+
+ $response = $validator->isValid(Query::parse('name.equal("Iron Man")'));
+
+ $this->assertEquals(false, $response);
+ $this->assertEquals('Attribute not found in schema: name', $validator->getDescription());
+ }
+
+ public function testAttributeWrongType()
+ {
+ $validator = new QueryValidator($this->schema);
+
+ $response = $validator->isValid(Query::parse('title.equal(1776)'));
+
+ $this->assertEquals(false, $response);
+ $this->assertEquals('Query type does not match expected: string', $validator->getDescription());
+ }
+}
diff --git a/tests/Database/Validator/StructureTest.php b/tests/Database/Validator/StructureTest.php
new file mode 100644
index 000000000..927bf5cd8
--- /dev/null
+++ b/tests/Database/Validator/StructureTest.php
@@ -0,0 +1,392 @@
+ Database::COLLECTIONS,
+ '$collection' => Database::COLLECTIONS,
+ 'name' => 'collections',
+ 'attributes' => [
+ [
+ '$id' => 'title',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 256,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'description',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 1000000,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'rating',
+ 'type' => Database::VAR_INTEGER,
+ 'format' => '',
+ 'size' => 5,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'price',
+ 'type' => Database::VAR_FLOAT,
+ 'format' => '',
+ 'size' => 5,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'published',
+ 'type' => Database::VAR_BOOLEAN,
+ 'format' => '',
+ 'size' => 5,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'tags',
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 55,
+ 'required' => false,
+ 'signed' => true,
+ 'array' => true,
+ 'filters' => [],
+ ],
+ [
+ '$id' => 'feedback',
+ 'type' => Database::VAR_STRING,
+ 'format' => 'email',
+ 'size' => 55,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ ],
+ 'indexes' => [],
+ ];
+
+ public function setUp(): void
+ {
+ Structure::addFormat('email', new Format(0), Database::VAR_STRING);
+ }
+
+ public function tearDown(): void
+ {
+ }
+
+ public function testDocumentInstance()
+ {
+ $validator = new Structure(new Document($this->collection));
+
+ $this->assertEquals(false, $validator->isValid('string'));
+ $this->assertEquals(false, $validator->isValid(null));
+ $this->assertEquals(false, $validator->isValid(false));
+ $this->assertEquals(false, $validator->isValid(1));
+
+ $this->assertEquals('Invalid document structure: Value must be an instance of Document', $validator->getDescription());
+ }
+
+ public function testCollectionAttribute()
+ {
+ $validator = new Structure(new Document());
+
+ $this->assertEquals(false, $validator->isValid(new Document()));
+
+ $this->assertEquals('Invalid document structure: Missing collection attribute $collection', $validator->getDescription());
+ }
+
+ public function testCollection()
+ {
+ $validator = new Structure(new Document());
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'Demo Title',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => true,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Collection "" not found', $validator->getDescription());
+ }
+
+ public function testRequiredKeys()
+ {
+ $validator = new Structure(new Document($this->collection));
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => true,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Missing required attribute "title"', $validator->getDescription());
+ }
+
+ public function testNullValues()
+ {
+ $validator = new Structure(new Document($this->collection));
+
+ $this->assertEquals(true, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'My Title',
+ 'description' => null,
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => true,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals(true, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'My Title',
+ 'description' => null,
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => true,
+ 'tags' => ['dog', null, 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+ }
+
+ public function testUnknownKeys()
+ {
+ $validator = new Structure(new Document($this->collection));
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'Demo Title',
+ 'titlex' => 'Unknown Attribute',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => true,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Unknown attribute: ""titlex"', $validator->getDescription());
+ }
+
+ public function testValidDocument()
+ {
+ $validator = new Structure(new Document($this->collection));
+
+ $this->assertEquals(true, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'Demo Title',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => true,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+ }
+
+ public function testStringValidation()
+ {
+ $validator = new Structure(new Document($this->collection));
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 5,
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => true,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Attribute "title" has invalid type. Value must be a string', $validator->getDescription());
+ }
+
+ public function testArrayOfStringsValidation()
+ {
+ $validator = new Structure(new Document($this->collection));
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'string',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => true,
+ 'tags' => [1, 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a string', $validator->getDescription());
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'string',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => true,
+ 'tags' => [true],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a string', $validator->getDescription());
+
+ $this->assertEquals(true, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'string',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => true,
+ 'tags' => [],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+ }
+
+ public function testIntegerValidation()
+ {
+ $validator = new Structure(new Document($this->collection));
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'string',
+ 'description' => 'Demo description',
+ 'rating' => true,
+ 'price' => 1.99,
+ 'published' => false,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Attribute "rating" has invalid type. Value must be a valid integer', $validator->getDescription());
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'string',
+ 'description' => 'Demo description',
+ 'rating' => '',
+ 'price' => 1.99,
+ 'published' => false,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Attribute "rating" has invalid type. Value must be a valid integer', $validator->getDescription());
+ }
+
+ public function testFloatValidation()
+ {
+ $validator = new Structure(new Document($this->collection));
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'string',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 2,
+ 'published' => false,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Attribute "price" has invalid type. Value must be a valid float', $validator->getDescription());
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'string',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => '',
+ 'published' => false,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Attribute "price" has invalid type. Value must be a valid float', $validator->getDescription());
+ }
+
+ public function testBooleanValidation()
+ {
+ $validator = new Structure(new Document($this->collection));
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'string',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => 1,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Attribute "published" has invalid type. Value must be a boolean', $validator->getDescription());
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'string',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => '',
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team@appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Attribute "published" has invalid type. Value must be a boolean', $validator->getDescription());
+ }
+
+ public function testFormatValidation()
+ {
+ $validator = new Structure(new Document($this->collection));
+
+ $this->assertEquals(false, $validator->isValid(new Document([
+ '$collection' => 'posts',
+ 'title' => 'string',
+ 'description' => 'Demo description',
+ 'rating' => 5,
+ 'price' => 1.99,
+ 'published' => true,
+ 'tags' => ['dog', 'cat', 'mouse'],
+ 'feedback' => 'team_appwrite.io',
+ ])));
+
+ $this->assertEquals('Invalid document structure: Attribute "feedback" has invalid format. Value must be a valid email address', $validator->getDescription());
+ }
+}
diff --git a/tests/Database/Validator/UIDTest.php b/tests/Database/Validator/UIDTest.php
new file mode 100644
index 000000000..8877eb31b
--- /dev/null
+++ b/tests/Database/Validator/UIDTest.php
@@ -0,0 +1,30 @@
+object = new UID();
+ }
+
+ public function tearDown(): void
+ {
+ }
+
+ public function testValues()
+ {
+ $this->assertEquals($this->object->isValid('5f058a8925807'), true);
+ $this->assertEquals($this->object->isValid('5f058a89258075f058a89258075f058t'), true);
+ $this->assertEquals($this->object->isValid('5f058a89258075f058a89258075f058tx'), false);
+ }
+}