added static php-crud-api
This commit is contained in:
3
seatmap-webapi/.gitignore
vendored
3
seatmap-webapi/.gitignore
vendored
@@ -1,3 +1,2 @@
|
||||
composer.phar
|
||||
vendor/
|
||||
data.db
|
||||
vendor/
|
||||
@@ -1,5 +0,0 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine on
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^(.*)$ api.php/$1 [QSA,L]
|
||||
</IfModule>
|
||||
@@ -1,15 +0,0 @@
|
||||
FROM php:apache
|
||||
|
||||
RUN docker-php-ext-install pdo pdo_mysql
|
||||
|
||||
RUN apt-get update; \
|
||||
apt-get install -y libpq5 libpq-dev; \
|
||||
docker-php-ext-install pdo pdo_pgsql; \
|
||||
apt-get autoremove --purge -y libpq-dev; \
|
||||
apt-get clean ; \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
|
||||
|
||||
RUN a2enmod rewrite
|
||||
|
||||
COPY api.php /var/www/html/api.php
|
||||
COPY .htaccess /var/www/html/.htaccess
|
||||
@@ -1,6 +1,6 @@
|
||||
# PHP-CRUD-API
|
||||
|
||||
Single file PHP 7 script that adds a REST API to a MySQL/MariaDB, PostgreSQL, SQL Server or SQLite database.
|
||||
Single file PHP 7 script that adds a REST API to a MySQL 5.6 InnoDB database. PostgreSQL 9.1 and MS SQL Server 2012 are fully supported.
|
||||
|
||||
NB: This is the [TreeQL](https://treeql.org) reference implementation in PHP.
|
||||
|
||||
@@ -27,11 +27,10 @@ There are also proof-of-concept ports of this script that only support basic RES
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 7.0 or higher with PDO drivers enabled for one of these database systems:
|
||||
- MySQL 5.6 / MariaDB 10.0 or higher for spatial features in MySQL
|
||||
- PostgreSQL 9.1 or higher with PostGIS 2.0 or higher for spatial features
|
||||
- SQL Server 2012 or higher (2017 for Linux support)
|
||||
- SQLite 3.16 or higher (spatial features NOT supported)
|
||||
- PHP 7.0 or higher with PDO drivers for MySQL, PgSQL or SqlSrv enabled
|
||||
- MySQL 5.6 / MariaDB 10.0 or higher for spatial features in MySQL
|
||||
- PostGIS 2.0 or higher for spatial features in PostgreSQL 9.1 or higher
|
||||
- SQL Server 2012 or higher (2017 for Linux support)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -53,11 +52,6 @@ Alternatively you can integrate this project into the web framework of your choi
|
||||
- [Automatic REST API for Symfony 4](https://tqdev.com/2019-automatic-rest-api-symfony)
|
||||
- [Automatic REST API for SlimPHP 4](https://tqdev.com/2019-automatic-api-slimphp-4)
|
||||
|
||||
In these integrations [Composer](https://getcomposer.org/) is used to load this project as a dependency.
|
||||
|
||||
For people that don't use composer, the file "`api.include.php`" is provided. This file contains everything
|
||||
from "`api.php`" except the configuration from "`src/index.php`" and can be used by PHP's "include".
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit the following lines in the bottom of the file "`api.php`":
|
||||
@@ -70,8 +64,8 @@ Edit the following lines in the bottom of the file "`api.php`":
|
||||
|
||||
These are all the configuration options and their default value between brackets:
|
||||
|
||||
- "driver": `mysql`, `pgsql`, `sqlsrv` or `sqlite` (`mysql`)
|
||||
- "address": Hostname (or filename) of the database server (`localhost`)
|
||||
- "driver": `mysql`, `pgsql` or `sqlsrv` (`mysql`)
|
||||
- "address": Hostname of the database server (`localhost`)
|
||||
- "port": TCP port of the database server (defaults to driver default)
|
||||
- "username": Username of the user connecting to the database (no default)
|
||||
- "password": Password of the user connecting to the database (no default)
|
||||
@@ -83,33 +77,19 @@ These are all the configuration options and their default value between brackets
|
||||
- "cacheType": `TempFile`, `Redis`, `Memcache`, `Memcached` or `NoCache` (`TempFile`)
|
||||
- "cachePath": Path/address of the cache (defaults to system's temp directory)
|
||||
- "cacheTime": Number of seconds the cache is valid (`10`)
|
||||
- "debug": Show errors in the "X-Exception" headers (`false`)
|
||||
- "debug": Show errors in the "X-Debug-Info" header (`false`)
|
||||
- "basePath": URI base path of the API (determined using PATH_INFO by default)
|
||||
|
||||
All configuration options are also available as environment variables. Write the config option with capitals, a "PHP_CRUD_API_" prefix and underscores for word breakes, so for instance:
|
||||
|
||||
- PHP_CRUD_API_DRIVER=mysql
|
||||
- PHP_CRUD_API_ADDRESS=localhost
|
||||
- PHP_CRUD_API_PORT=3306
|
||||
- PHP_CRUD_API_DATABASE=php-crud-api
|
||||
- PHP_CRUD_API_USERNAME=php-crud-api
|
||||
- PHP_CRUD_API_PASSWORD=php-crud-api
|
||||
- PHP_CRUD_API_DEBUG=1
|
||||
|
||||
The environment variables take precedence over the PHP configuration.
|
||||
|
||||
## Limitations
|
||||
|
||||
These limitation and constrains apply:
|
||||
|
||||
- Primary keys should either be auto-increment (from 1 to 2^53) or UUID
|
||||
- Composite primary and composite foreign keys are not supported
|
||||
- Composite primary or foreign keys are not supported
|
||||
- Complex writes (transactions) are not supported
|
||||
- Complex queries calling functions (like "concat" or "sum") are not supported
|
||||
- Database must support and define foreign key constraints
|
||||
- SQLite cannot have bigint typed auto incrementing primary keys
|
||||
- SQLite does not support altering table columns (structure)
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
The following features are supported:
|
||||
@@ -119,7 +99,7 @@ The following features are supported:
|
||||
- Supports POST variables as input (x-www-form-urlencoded)
|
||||
- Supports a JSON object as input
|
||||
- Supports a JSON array as input (batch insert)
|
||||
- Sanitize and validate input using type rules and callbacks
|
||||
- Sanitize and validate input using callbacks
|
||||
- Permission system for databases, tables, columns and records
|
||||
- Multi-tenant single and multi database layouts are supported
|
||||
- Multi-domain CORS support for cross-domain requests
|
||||
@@ -292,9 +272,8 @@ Examples of filter usage are:
|
||||
GET /records/categories?filter=name,eq,Internet
|
||||
GET /records/categories?filter=name,sw,Inter
|
||||
GET /records/categories?filter=id,le,1
|
||||
GET /records/categories?filter=id,ngt,1
|
||||
GET /records/categories?filter=id,bt,0,1
|
||||
GET /records/categories?filter=id,in,0,1
|
||||
GET /records/categories?filter=id,ngt,2
|
||||
GET /records/categories?filter=id,bt,1,1
|
||||
|
||||
Output:
|
||||
|
||||
@@ -323,7 +302,7 @@ As you see we added a number to the "filter" parameter to indicate that "OR" ins
|
||||
Note that you can also repeat "filter1" and create an "AND" within an "OR". Since you can also go one level deeper
|
||||
by adding a letter (a-f) you can create almost any reasonably complex condition tree.
|
||||
|
||||
NB: You can only filter on the requested table (not on it's included tables) and filters are only applied on list calls.
|
||||
NB: You can only filter on the requested table (not on it's included) and filters are only applied on list calls.
|
||||
|
||||
### Column selection
|
||||
|
||||
@@ -605,7 +584,6 @@ The GeoJSON functionality is enabled by default, but can be disabled using the "
|
||||
You can enable the following middleware using the "middlewares" config parameter:
|
||||
|
||||
- "firewall": Limit access to specific IP addresses
|
||||
- "sslRedirect": Force connection over HTTPS instead of HTTP
|
||||
- "cors": Support for CORS requests (enabled by default)
|
||||
- "xsrf": Block XSRF attacks using the 'Double Submit Cookie' method
|
||||
- "ajaxOnly": Restrict non-AJAX requests to prevent XSRF attacks
|
||||
@@ -614,14 +592,13 @@ You can enable the following middleware using the "middlewares" config parameter
|
||||
- "basicAuth": Support for "Basic Authentication"
|
||||
- "reconnect": Reconnect to the database with different parameters
|
||||
- "authorization": Restrict access to certain tables or columns
|
||||
- "validation": Return input validation errors for custom rules and default type rules
|
||||
- "validation": Return input validation errors for custom rules
|
||||
- "ipAddress": Fill a protected field with the IP address on create
|
||||
- "sanitation": Apply input sanitation on create and update
|
||||
- "multiTenancy": Restricts tenants access in a multi-tenant scenario
|
||||
- "pageLimits": Restricts list operations to prevent database scraping
|
||||
- "joinLimits": Restricts join parameters to prevent database scraping
|
||||
- "customization": Provides handlers for request and response customization
|
||||
- "xml": Translates all input and output from JSON to XML
|
||||
|
||||
The "middlewares" config parameter is a comma separated list of enabled middlewares.
|
||||
You can tune the middleware behavior using middleware specific configuration parameters:
|
||||
@@ -629,7 +606,7 @@ You can tune the middleware behavior using middleware specific configuration par
|
||||
- "firewall.reverseProxy": Set to "true" when a reverse proxy is used ("")
|
||||
- "firewall.allowedIpAddresses": List of IP addresses that are allowed to connect ("")
|
||||
- "cors.allowedOrigins": The origins allowed in the CORS headers ("*")
|
||||
- "cors.allowHeaders": The headers allowed in the CORS request ("Content-Type, X-XSRF-TOKEN, X-Authorization")
|
||||
- "cors.allowHeaders": The headers allowed in the CORS request ("Content-Type, X-XSRF-TOKEN")
|
||||
- "cors.allowMethods": The methods allowed in the CORS request ("OPTIONS, GET, PUT, POST, DELETE, PATCH")
|
||||
- "cors.allowCredentials": To allow credentials in the CORS request ("true")
|
||||
- "cors.exposeHeaders": Whitelist headers that browsers are allowed to access ("")
|
||||
@@ -645,8 +622,6 @@ You can tune the middleware behavior using middleware specific configuration par
|
||||
- "dbAuth.usernameColumn": The users table column that holds usernames ("username")
|
||||
- "dbAuth.passwordColumn": The users table column that holds passwords ("password")
|
||||
- "dbAuth.returnedColumns": The columns returned on successful login, empty means 'all' ("")
|
||||
- "dbAuth.registerUser": JSON user data (or "1") in case you want the /register endpoint enabled ("")
|
||||
- "dbAuth.passwordLength": Minimum length that the password must have ("12")
|
||||
- "dbAuth.sessionName": The name of the PHP session that is started ("")
|
||||
- "jwtAuth.mode": Set to "optional" if you want to allow anonymous access ("required")
|
||||
- "jwtAuth.header": Name of the header containing the JWT token ("X-Authorization")
|
||||
@@ -670,16 +645,11 @@ You can tune the middleware behavior using middleware specific configuration par
|
||||
- "reconnect.passwordHandler": Handler to implement retrieval of the database password ("")
|
||||
- "authorization.tableHandler": Handler to implement table authorization rules ("")
|
||||
- "authorization.columnHandler": Handler to implement column authorization rules ("")
|
||||
- "authorization.pathHandler": Handler to implement path authorization rules ("")
|
||||
- "authorization.recordHandler": Handler to implement record authorization filter rules ("")
|
||||
- "validation.handler": Handler to implement validation rules for input values ("")
|
||||
- "validation.types": Types to enable type validation for, empty means 'none' ("all")
|
||||
- "validation.tables": Tables to enable type validation for, empty means 'none' ("all")
|
||||
- "ipAddress.tables": Tables to search for columns to override with IP address ("")
|
||||
- "ipAddress.columns": Columns to protect and override with the IP address on create ("")
|
||||
- "sanitation.handler": Handler to implement sanitation rules for input values ("")
|
||||
- "sanitation.types": Types to enable type sanitation for, empty means 'none' ("all")
|
||||
- "sanitation.tables": Tables to enable type sanitation for, empty means 'none' ("all")
|
||||
- "multiTenancy.handler": Handler to implement simple multi-tenancy rules ("")
|
||||
- "pageLimits.pages": The maximum page number that a list operation allows ("100")
|
||||
- "pageLimits.records": The maximum number of records returned by a list operation ("1000")
|
||||
@@ -688,7 +658,6 @@ You can tune the middleware behavior using middleware specific configuration par
|
||||
- "joinLimits.records": The maximum number of records returned for a joined entity ("1000")
|
||||
- "customization.beforeHandler": Handler to implement request customization ("")
|
||||
- "customization.afterHandler": Handler to implement response customization ("")
|
||||
- "xml.types": JSON types that should be added to the XML type attribute ("null,array")
|
||||
|
||||
If you don't specify these parameters in the configuration, then the default values (between brackets) are used.
|
||||
|
||||
@@ -710,22 +679,20 @@ Below you find more information on each of the authentication types.
|
||||
|
||||
#### Database authentication
|
||||
|
||||
The database authentication middleware defines three new routes:
|
||||
The database authentication middleware defines two new routes:
|
||||
|
||||
method path - parameters - description
|
||||
---------------------------------------------------------------------------------------------------
|
||||
GET /me - - returns the user that is currently logged in
|
||||
POST /register - username, password - adds a user with given username and password
|
||||
POST /login - username, password - logs a user in by username and password
|
||||
POST /password - username, password, newPassword - updates the password of the logged in user
|
||||
POST /logout - - logs out the currently logged in user
|
||||
method path - parameters - description
|
||||
----------------------------------------------------------------------------------------
|
||||
POST /login - username + password - logs a user in by username and password
|
||||
POST /logout - - logs out the currently logged in user
|
||||
|
||||
A user can be logged in by sending it's username and password to the login endpoint (in JSON format).
|
||||
The authenticated user (with all it's properties) will be stored in the `$_SESSION['user']` variable.
|
||||
The user can be logged out by sending a POST request with an empty body to the logout endpoint.
|
||||
The passwords are stored as hashes in the password column in the users table. You can register a new user
|
||||
using the register endpoint, but this functionality must be turned on using the "dbAuth.regsiterUser"
|
||||
configuration parameter.
|
||||
The passwords are stored as hashes in the password column in the users table. To generate the hash value
|
||||
for the password 'pass2' you can run on the command line:
|
||||
|
||||
php -r 'echo password_hash("pass2", PASSWORD_DEFAULT)."\n";'
|
||||
|
||||
It is IMPORTANT to restrict access to the users table using the 'authorization' middleware, otherwise all
|
||||
users can freely add, modify or delete any account! The minimal configuration is shown below:
|
||||
@@ -860,7 +827,7 @@ Add the "columns" controller in the configuration to enable this functionality.
|
||||
|
||||
### Authorizing tables, columns and records
|
||||
|
||||
By default all tables, columns and paths are accessible. If you want to restrict access to some tables you may add the 'authorization' middleware
|
||||
By default all tables and columns are accessible. If you want to restrict access to some tables you may add the 'authorization' middleware
|
||||
and define a 'authorization.tableHandler' function that returns 'false' for these tables.
|
||||
|
||||
'authorization.tableHandler' => function ($operation, $tableName) {
|
||||
@@ -882,12 +849,6 @@ The above example will restrict access to the 'password' field of the 'users' ta
|
||||
The above example will disallow access to user records where the username is 'admin'.
|
||||
This construct adds a filter to every executed query.
|
||||
|
||||
'authorization.pathHandler' => function ($path) {
|
||||
return $path === 'openapi' ? false : true;
|
||||
},
|
||||
|
||||
The above example will disabled the `/openapi` route.
|
||||
|
||||
NB: You need to handle the creation of invalid records with a validation (or sanitation) handler.
|
||||
|
||||
### SQL GRANT authorization
|
||||
@@ -904,7 +865,7 @@ should not use the "authorization" middleware, but you do need to use the "recon
|
||||
},
|
||||
|
||||
This will make the API connect to the database specifying "mevdschee" as the username and "secret123" as the password.
|
||||
The OpenAPI specification is less specific on allowed and disallowed operations when you are using database permissions,
|
||||
The OpenAPI specification is less specific on allowed and disallowed operations, when you are using database permissions,
|
||||
as the permissions are not read in the reflection step.
|
||||
|
||||
NB: You may want to retrieve the username and password from the session (the "$_SESSION" variable).
|
||||
@@ -920,28 +881,10 @@ the 'sanitation' middleware and define a 'sanitation.handler' function that retu
|
||||
|
||||
The above example will strip all HTML tags from strings in the input.
|
||||
|
||||
### Type sanitation
|
||||
|
||||
If you enable the 'sanitation' middleware, then you (automatically) also enable type sanitation. When this is enabled you may:
|
||||
|
||||
- send leading and trailing whitespace in a non-character field (it will be ignored).
|
||||
- send a float to an integer or bigint field (it will be rounded).
|
||||
- send a base64url encoded string (it will be converted to regular base64 encoding).
|
||||
- send a time/date/timestamp in any [strtotime accepted format](https://www.php.net/manual/en/datetime.formats.php) (it will be converted).
|
||||
|
||||
You may use the config settings "`sanitation.types`" and "`sanitation.tables`"' to define for which types and
|
||||
in which tables you want to apply type sanitation (defaults to 'all'). Example:
|
||||
|
||||
'sanitation.types' => 'date,timestamp',
|
||||
'sanitation.tables' => 'posts,comments',
|
||||
|
||||
Here we enable the type sanitation for date and timestamp fields in the posts and comments tables.
|
||||
|
||||
### Validating input
|
||||
|
||||
By default all input is accepted and sent to the database. If you want to validate the input in a custom way,
|
||||
you may add the 'validation' middleware and define a 'validation.handler' function that returns a boolean
|
||||
indicating whether or not the value is valid.
|
||||
By default all input is accepted. If you want to validate the input, you may add the 'validation' middleware and define a
|
||||
'validation.handler' function that returns a boolean indicating whether or not the value is valid.
|
||||
|
||||
'validation.handler' => function ($operation, $tableName, $column, $value, $context) {
|
||||
return ($column['name'] == 'post_id' && !is_numeric($value)) ? 'must be numeric' : true;
|
||||
@@ -967,37 +910,6 @@ Then the server will return a '422' HTTP status code and nice error message:
|
||||
|
||||
You can parse this output to make form fields show up with a red border and their appropriate error message.
|
||||
|
||||
### Type validations
|
||||
|
||||
If you enable the 'validation' middleware, then you (automatically) also enable type validation.
|
||||
This includes the following error messages:
|
||||
|
||||
| error message | reason | applies to types |
|
||||
| ------------------- | --------------------------- | ------------------------------------------- |
|
||||
| cannot be null | unexpected null value | (any non-nullable column) |
|
||||
| illegal whitespace | leading/trailing whitespace | integer bigint decimal float double boolean |
|
||||
| invalid integer | illegal characters | integer bigint |
|
||||
| string too long | too many characters | varchar varbinary |
|
||||
| invalid decimal | illegal characters | decimal |
|
||||
| decimal too large | too many digits before dot | decimal |
|
||||
| decimal too precise | too many digits after dot | decimal |
|
||||
| invalid float | illegal characters | float double |
|
||||
| invalid boolean | use 1, 0, true or false | boolean |
|
||||
| invalid date | use yyyy-mm-dd | date |
|
||||
| invalid time | use hh:mm:ss | time |
|
||||
| invalid timestamp | use yyyy-mm-dd hh:mm:ss | timestamp |
|
||||
| invalid base64 | illegal characters | varbinary, blob |
|
||||
|
||||
You may use the config settings "`validation.types`" and "`validation.tables`"' to define for which types and
|
||||
in which tables you want to apply type validation (defaults to 'all'). Example:
|
||||
|
||||
'validation.types' => 'date,timestamp',
|
||||
'validation.tables' => 'posts,comments',
|
||||
|
||||
Here we enable the type validation for date and timestamp fields in the posts and comments tables.
|
||||
|
||||
NB: Types that are enabled will be checked for null values when the column is non-nullable.
|
||||
|
||||
### Multi-tenancy support
|
||||
|
||||
Two forms of multi-tenancy are supported:
|
||||
@@ -1066,36 +978,6 @@ You may use the "customization" middleware to modify request and response and im
|
||||
|
||||
The above example will add a header "X-Time-Taken" with the number of seconds the API call has taken.
|
||||
|
||||
### XML middleware
|
||||
|
||||
You may use the "xml" middleware to translate input and output from JSON to XML. This request:
|
||||
|
||||
GET /records/posts/1
|
||||
|
||||
Outputs (when "pretty printed"):
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"user_id": 1,
|
||||
"category_id": 1,
|
||||
"content": "blog started"
|
||||
}
|
||||
|
||||
While (note the "format" query parameter):
|
||||
|
||||
GET /records/posts/1?format=xml
|
||||
|
||||
Outputs:
|
||||
|
||||
<root>
|
||||
<id>1</id>
|
||||
<user_id>1</user_id>
|
||||
<category_id>1</category_id>
|
||||
<content>blog started</content>
|
||||
</root>
|
||||
|
||||
This functionality is disabled by default and must be enabled using the "middlewares" configuration setting.
|
||||
|
||||
### File uploads
|
||||
|
||||
File uploads are supported through the [FileReader API](https://caniuse.com/#feat=filereader), check out the [example](https://github.com/mevdschee/php-crud-api/blob/master/examples/clients/upload/vanilla.html).
|
||||
@@ -1131,39 +1013,42 @@ in case that you use a non-default "cacheType" the hostname (optionally with por
|
||||
|
||||
## Types
|
||||
|
||||
These are the supported types with their length, category, JSON type and format:
|
||||
These are the supported types with their default length/precision/scale:
|
||||
|
||||
| type | length | category | JSON type | format |
|
||||
| ---------- | ------ | --------- | --------- | ------------------- |
|
||||
| varchar | 255 | character | string | |
|
||||
| clob | | character | string | |
|
||||
| boolean | | boolean | boolean | |
|
||||
| integer | | integer | number | |
|
||||
| bigint | | integer | number | |
|
||||
| float | | float | number | |
|
||||
| double | | float | number | |
|
||||
| decimal | 19,4 | decimal | string | |
|
||||
| date | | date/time | string | yyyy-mm-dd |
|
||||
| time | | date/time | string | hh:mm:ss |
|
||||
| timestamp | | date/time | string | yyyy-mm-dd hh:mm:ss |
|
||||
| varbinary | 255 | binary | string | base64 encoded |
|
||||
| blob | | binary | string | base64 encoded |
|
||||
| geometry | | other | string | well-known text |
|
||||
character types
|
||||
- varchar(255)
|
||||
- clob
|
||||
|
||||
Note that geometry is a non-jdbc type and thus has limited support.
|
||||
boolean types:
|
||||
- boolean
|
||||
|
||||
## Data types in JavaScript
|
||||
integer types:
|
||||
- integer
|
||||
- bigint
|
||||
|
||||
Javascript and Javascript object notation (JSON) are not very well suited for reading database records. Decimal, date/time, binary and geometry types must be represented as strings in JSON (binary is base64 encoded, geometries are in WKT format). Below are two more serious issues described.
|
||||
floating point types:
|
||||
- float
|
||||
- double
|
||||
|
||||
### 64 bit integers
|
||||
decimal types:
|
||||
- decimal(19,4)
|
||||
|
||||
date/time types:
|
||||
- date
|
||||
- time
|
||||
- timestamp
|
||||
|
||||
binary types:
|
||||
- varbinary(255)
|
||||
- blob
|
||||
|
||||
other types:
|
||||
- geometry /* non-jdbc type, extension with limited support */
|
||||
|
||||
## 64 bit integers in JavaScript
|
||||
|
||||
JavaScript does not support 64 bit integers. All numbers are stored as 64 bit floating point values. The mantissa of a 64 bit floating point number is only 53 bit and that is why all integer numbers bigger than 53 bit may cause problems in JavaScript.
|
||||
|
||||
### Inf and NaN floats
|
||||
|
||||
The valid floating point values 'Infinite' (calculated with '1/0') and 'Not a Number' (calculated with '0/0') cannot be expressed in JSON, as they are not supported by the [JSON specification](https://www.json.org). When these values are stored in a database then you cannot read them as this script outputs database records as JSON.
|
||||
|
||||
## Errors
|
||||
|
||||
The following errors may be reported:
|
||||
@@ -1205,12 +1090,10 @@ NB: Any non-error response will have status: 200 OK
|
||||
|
||||
I am testing mainly on Ubuntu and I have the following test setups:
|
||||
|
||||
- (Docker) Debian 10 with PHP 7.3, MariaDB 10.3, PostgreSQL 11.4 (PostGIS 2.5)
|
||||
- (Docker) Debian 9 with PHP 7.0, MariaDB 10.1, PostgreSQL 9.6 (PostGIS 2.3)
|
||||
- (Docker) Ubuntu 16.04 with PHP 7.0, MariaDB 10.0, PostgreSQL 9.5 (PostGIS 2.2) and SQL Server 2017
|
||||
- (Docker) Debian 9 with PHP 7.0, MariaDB 10.1, PostgreSQL 9.6 (PostGIS 2.3) and SQLite 3.16
|
||||
- (Docker) Ubuntu 18.04 with PHP 7.2, MySQL 5.7, PostgreSQL 10.4 (PostGIS 2.4) and SQLite 3.22
|
||||
- (Docker) Debian 10 with PHP 7.3, MariaDB 10.3, PostgreSQL 11.4 (PostGIS 2.5) and SQLite 3.27
|
||||
- (Docker) Ubuntu 20.04 with PHP 7.4, MySQL 8.0, PostgreSQL 12.2 (PostGIS 3.0) and SQLite 3.31
|
||||
- (Docker) CentOS 8 with PHP 7.4, MariaDB 10.5, PostgreSQL 12.5 (PostGIS 3.0) and SQLite 3.26
|
||||
- (Docker) Ubuntu 18.04 with PHP 7.2, MySQL 5.7, PostgreSQL 10.4 (PostGIS 2.4)
|
||||
|
||||
This covers not all environments (yet), so please notify me of failing tests and report your environment.
|
||||
I will try to cover most relevant setups in the "docker" folder of the project.
|
||||
@@ -1254,7 +1137,7 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
### Docker tests
|
||||
### Docker
|
||||
|
||||
Install docker using the following commands and then logout and login for the changes to take effect:
|
||||
|
||||
@@ -1263,18 +1146,6 @@ Install docker using the following commands and then logout and login for the ch
|
||||
|
||||
To run the docker tests run "build_all.sh" and "run_all.sh" from the docker directory. The output should be:
|
||||
|
||||
================================================
|
||||
CentOS 8 (PHP 7.4)
|
||||
================================================
|
||||
[1/4] Starting MariaDB 10.5 ..... done
|
||||
[2/4] Starting PostgreSQL 12.5 .. done
|
||||
[3/4] Starting SQLServer 2017 ... skipped
|
||||
[4/4] Cloning PHP-CRUD-API v2 ... skipped
|
||||
------------------------------------------------
|
||||
mysql: 110 tests ran in 1911 ms, 1 skipped, 0 failed
|
||||
pgsql: 110 tests ran in 1112 ms, 1 skipped, 0 failed
|
||||
sqlsrv: skipped, driver not loaded
|
||||
sqlite: 110 tests ran in 1178 ms, 12 skipped, 0 failed
|
||||
================================================
|
||||
Debian 10 (PHP 7.3)
|
||||
================================================
|
||||
@@ -1283,10 +1154,9 @@ To run the docker tests run "build_all.sh" and "run_all.sh" from the docker dire
|
||||
[3/4] Starting SQLServer 2017 ... skipped
|
||||
[4/4] Cloning PHP-CRUD-API v2 ... skipped
|
||||
------------------------------------------------
|
||||
mysql: 110 tests ran in 3459 ms, 1 skipped, 0 failed
|
||||
pgsql: 110 tests ran in 1134 ms, 1 skipped, 0 failed
|
||||
mysql: 100 tests ran in 3623 ms, 0 failed
|
||||
pgsql: 100 tests ran in 1310 ms, 0 failed
|
||||
sqlsrv: skipped, driver not loaded
|
||||
sqlite: 110 tests ran in 1275 ms, 12 skipped, 0 failed
|
||||
================================================
|
||||
Debian 9 (PHP 7.0)
|
||||
================================================
|
||||
@@ -1295,10 +1165,9 @@ To run the docker tests run "build_all.sh" and "run_all.sh" from the docker dire
|
||||
[3/4] Starting SQLServer 2017 ... skipped
|
||||
[4/4] Cloning PHP-CRUD-API v2 ... skipped
|
||||
------------------------------------------------
|
||||
mysql: 110 tests ran in 3181 ms, 1 skipped, 0 failed
|
||||
pgsql: 110 tests ran in 1201 ms, 1 skipped, 0 failed
|
||||
mysql: 100 tests ran in 4844 ms, 0 failed
|
||||
pgsql: 100 tests ran in 1394 ms, 0 failed
|
||||
sqlsrv: skipped, driver not loaded
|
||||
sqlite: 110 tests ran in 1414 ms, 12 skipped, 0 failed
|
||||
================================================
|
||||
Ubuntu 16.04 (PHP 7.0)
|
||||
================================================
|
||||
@@ -1307,10 +1176,9 @@ To run the docker tests run "build_all.sh" and "run_all.sh" from the docker dire
|
||||
[3/4] Starting SQLServer 2017 ... done
|
||||
[4/4] Cloning PHP-CRUD-API v2 ... skipped
|
||||
------------------------------------------------
|
||||
mysql: 110 tests ran in 3168 ms, 1 skipped, 0 failed
|
||||
pgsql: 110 tests ran in 1197 ms, 1 skipped, 0 failed
|
||||
sqlsrv: 110 tests ran in 10151 ms, 1 skipped, 0 failed
|
||||
sqlite: skipped, driver not loaded
|
||||
mysql: 100 tests ran in 4932 ms, 0 failed
|
||||
pgsql: 100 tests ran in 1394 ms, 0 failed
|
||||
sqlsrv: 100 tests ran in 50977 ms, 0 failed
|
||||
================================================
|
||||
Ubuntu 18.04 (PHP 7.2)
|
||||
================================================
|
||||
@@ -1319,33 +1187,18 @@ To run the docker tests run "build_all.sh" and "run_all.sh" from the docker dire
|
||||
[3/4] Starting SQLServer 2017 ... skipped
|
||||
[4/4] Cloning PHP-CRUD-API v2 ... skipped
|
||||
------------------------------------------------
|
||||
mysql: 110 tests ran in 3709 ms, 1 skipped, 0 failed
|
||||
pgsql: 110 tests ran in 1334 ms, 1 skipped, 0 failed
|
||||
mysql: 100 tests ran in 4327 ms, 0 failed
|
||||
pgsql: 100 tests ran in 1396 ms, 0 failed
|
||||
sqlsrv: skipped, driver not loaded
|
||||
sqlite: 110 tests ran in 1477 ms, 12 skipped, 0 failed
|
||||
================================================
|
||||
Ubuntu 20.04 (PHP 7.4)
|
||||
================================================
|
||||
[1/4] Starting MySQL 8.0 ........ done
|
||||
[2/4] Starting PostgreSQL 12.2 .. done
|
||||
[3/4] Starting SQLServer 2017 ... skipped
|
||||
[4/4] Cloning PHP-CRUD-API v2 ... skipped
|
||||
------------------------------------------------
|
||||
mysql: 110 tests ran in 5102 ms, 1 skipped, 0 failed
|
||||
pgsql: 110 tests ran in 1170 ms, 1 skipped, 0 failed
|
||||
sqlsrv: skipped, driver not loaded
|
||||
sqlite: 110 tests ran in 1380 ms, 12 skipped, 0 failed
|
||||
|
||||
The above test run (including starting up the databases) takes less than 5 minutes on my slow laptop.
|
||||
|
||||
$ ./run.sh
|
||||
1) centos8
|
||||
2) debian10
|
||||
3) debian9
|
||||
4) ubuntu16
|
||||
5) ubuntu18
|
||||
6) ubuntu20
|
||||
> 5
|
||||
$ ./run.sh
|
||||
1) debian10
|
||||
2) debian9
|
||||
3) ubuntu16
|
||||
4) ubuntu18
|
||||
> 4
|
||||
================================================
|
||||
Ubuntu 18.04 (PHP 7.2)
|
||||
================================================
|
||||
@@ -1354,36 +1207,11 @@ The above test run (including starting up the databases) takes less than 5 minut
|
||||
[3/4] Starting SQLServer 2017 ... skipped
|
||||
[4/4] Cloning PHP-CRUD-API v2 ... skipped
|
||||
------------------------------------------------
|
||||
mysql: 105 tests ran in 3390 ms, 1 skipped, 0 failed
|
||||
pgsql: 105 tests ran in 936 ms, 1 skipped, 0 failed
|
||||
mysql: 100 tests ran in 4327 ms, 0 failed
|
||||
pgsql: 100 tests ran in 1396 ms, 0 failed
|
||||
sqlsrv: skipped, driver not loaded
|
||||
sqlite: 105 tests ran in 1063 ms, 12 skipped, 0 failed
|
||||
root@b7ab9472e08f:/php-crud-api#
|
||||
|
||||
As you can see the "run.sh" script gives you access to a prompt in a chosen the docker environment.
|
||||
In this environment the local files are mounted. This allows for easy debugging on different environments.
|
||||
You may type "exit" when you are done.
|
||||
|
||||
### Docker image
|
||||
|
||||
There is a `Dockerfile` in the repository that is used to build an image at:
|
||||
|
||||
[https://hub.docker.com/r/mevdschee/php-crud-api](https://hub.docker.com/r/mevdschee/php-crud-api)
|
||||
|
||||
It will be automatically build on every release. The "latest" tag points to the last release.
|
||||
|
||||
### Docker compose
|
||||
|
||||
This repository also contains a `docker-compose.yml` file that you can install/build/run using:
|
||||
|
||||
sudo apt install docker-compose
|
||||
docker-compose build
|
||||
docker-compose up
|
||||
|
||||
This will setup a database (MySQL) and a webserver (Apache) and runs the application using the blog example data used in the tests.
|
||||
|
||||
Test the script (running in the container) by opening the following URL:
|
||||
|
||||
http://localhost:8080/records/posts/1
|
||||
|
||||
Enjoy!
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -93,8 +93,6 @@ function run(string $base, array $dirs, string $filename, array $ignore)
|
||||
include 'tmp_' . $filename;
|
||||
ob_end_clean();
|
||||
rename('tmp_' . $filename, $filename);
|
||||
$data = substr($data, 0, strrpos($data, "\n// file: src/index.php"));
|
||||
file_put_contents(str_replace('.php', '.include.php', $filename), $data);
|
||||
$end = microtime(true);
|
||||
$time = ($end - $start) * 1000;
|
||||
echo sprintf("%d files combined in %d ms into '%s'\n", $count, $time, $filename);
|
||||
@@ -104,10 +102,4 @@ $ignore = [
|
||||
'vendor/nyholm/psr7/src/Factory/HttplugFactory.php',
|
||||
];
|
||||
|
||||
$directories = ['vendor/nyholm', 'src'];
|
||||
|
||||
if (!extension_loaded('psr')) {
|
||||
array_unshift($directories, 'vendor/psr');
|
||||
}
|
||||
|
||||
run(__DIR__, $directories, 'api.php', $ignore);
|
||||
run(__DIR__, ['vendor/psr', 'vendor/nyholm', 'src'], 'api.php', $ignore);
|
||||
|
||||
3
seatmap-webapi/composer.lock
generated
3
seatmap-webapi/composer.lock
generated
@@ -391,6 +391,5 @@
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "1.1.0"
|
||||
"platform-dev": []
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
database:
|
||||
image: mysql:8.0
|
||||
container_name: database
|
||||
restart: always
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=php-crud-api
|
||||
- MYSQL_DATABASE=php-crud-api
|
||||
- MYSQL_USER=php-crud-api
|
||||
- MYSQL_PASSWORD=php-crud-api
|
||||
#ports:
|
||||
#- "33066:3306"
|
||||
volumes:
|
||||
- ./tests/fixtures/blog_mysql.sql:/docker-entrypoint-initdb.d/blog_mysql.sql
|
||||
webserver:
|
||||
container_name: webserver
|
||||
build:
|
||||
context: ./
|
||||
environment:
|
||||
#- PHP_CRUD_API_DRIVER=mysql
|
||||
- PHP_CRUD_API_ADDRESS=database
|
||||
#- PHP_CRUD_API_PORT=3306
|
||||
#- PHP_CRUD_API_DATABASE=php-crud-api
|
||||
#- PHP_CRUD_API_USERNAME=php-crud-api
|
||||
#- PHP_CRUD_API_PASSWORD=php-crud-api
|
||||
#- PHP_CRUD_API_DEBUG=1
|
||||
ports:
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- database
|
||||
@@ -1,36 +0,0 @@
|
||||
FROM centos:8
|
||||
|
||||
# add this to avoid locale warnings
|
||||
RUN dnf -y install glibc-locale-source glibc-langpack-en
|
||||
RUN localedef -i en_US -f UTF-8 en_US.UTF-8
|
||||
|
||||
# add utils for repos
|
||||
RUN dnf -y install wget dnf-utils
|
||||
|
||||
# enable remi repo for php
|
||||
RUN dnf -y install http://rpms.remirepo.net/enterprise/remi-release-8.rpm
|
||||
# enable mariadb repo
|
||||
RUN wget https://downloads.mariadb.com/MariaDB/mariadb_repo_setup && bash mariadb_repo_setup
|
||||
# enable the postgresql repo
|
||||
RUN dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
|
||||
# enable epel repo
|
||||
RUN dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
|
||||
# enable powertools repos
|
||||
RUN dnf -y install 'dnf-command(config-manager)' && dnf -y config-manager --set-enabled PowerTools
|
||||
|
||||
# set php to remi 7.4
|
||||
RUN dnf -y module reset php && dnf -y module enable php:remi-7.4
|
||||
# disable mariadb and postgresql default (appstream) repo
|
||||
RUN dnf -y module disable mariadb
|
||||
RUN dnf -y module disable postgresql
|
||||
|
||||
RUN dnf -y install \
|
||||
php-cli php-xml php-json php-mbstring \
|
||||
MariaDB-server MariaDB-client php-mysqlnd \
|
||||
postgresql12 postgresql12-server php-pgsql postgis30_12 \
|
||||
sqlite php-sqlite3 \
|
||||
git wget
|
||||
|
||||
# install run script
|
||||
ADD run.sh /usr/sbin/docker-run
|
||||
CMD docker-run
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "================================================"
|
||||
echo " CentOS 8 (PHP 7.4)"
|
||||
echo "================================================"
|
||||
echo -n "[1/4] Starting MariaDB 10.5 ..... "
|
||||
# initialize mysql
|
||||
mysql_install_db > /dev/null
|
||||
chown -R mysql:mysql /var/lib/mysql
|
||||
# run mysql server
|
||||
nohup /usr/sbin/mysqld -u mysql > /root/mysql.log 2>&1 &
|
||||
# wait for mysql to become available
|
||||
while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
|
||||
sleep 1
|
||||
done
|
||||
# create database and user on mysql
|
||||
mysql -u root >/dev/null << 'EOF'
|
||||
CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
|
||||
GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
echo "done"
|
||||
|
||||
echo -n "[2/4] Starting PostgreSQL 12.5 .. "
|
||||
# initialize postgresql
|
||||
su - -c "/usr/pgsql-12/bin/initdb --auth-local peer --auth-host password -D /var/lib/pgsql/data" postgres > /dev/null
|
||||
# run postgres server
|
||||
nohup su - -c "/usr/pgsql-12/bin/postgres -D /var/lib/pgsql/data" postgres > /root/postgres.log 2>&1 &
|
||||
# wait for postgres to become available
|
||||
until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
|
||||
sleep 1;
|
||||
done
|
||||
# create database and user on postgres
|
||||
su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
|
||||
CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
|
||||
CREATE DATABASE "php-crud-api";
|
||||
GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
|
||||
\c "php-crud-api";
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
\q
|
||||
EOF
|
||||
echo "done"
|
||||
|
||||
echo -n "[3/4] Starting SQLServer 2017 ... "
|
||||
echo "skipped"
|
||||
|
||||
echo -n "[4/4] Cloning PHP-CRUD-API v2 ... "
|
||||
# install software
|
||||
if [ -d /php-crud-api ]; then
|
||||
echo "skipped"
|
||||
else
|
||||
git clone --quiet https://github.com/mevdschee/php-crud-api.git
|
||||
echo "done"
|
||||
fi
|
||||
|
||||
echo "------------------------------------------------"
|
||||
|
||||
# run the tests
|
||||
cd php-crud-api
|
||||
php test.php
|
||||
@@ -2,13 +2,12 @@ FROM debian:10
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# install: php / mysql / postgres / sqlite / tools / mssql deps
|
||||
# install: php / mysql / postgres / tools / mssql deps
|
||||
RUN apt-get update && apt-get -y install \
|
||||
php-cli php-xml php-mbstring \
|
||||
php-cli php-xml \
|
||||
mariadb-server mariadb-client php-mysql \
|
||||
postgresql php-pgsql \
|
||||
postgresql-11-postgis-2.5 \
|
||||
sqlite3 php-sqlite3 \
|
||||
git wget
|
||||
|
||||
# install run script
|
||||
|
||||
@@ -14,7 +14,7 @@ while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
|
||||
done
|
||||
# create database and user on mysql
|
||||
mysql -u root >/dev/null << 'EOF'
|
||||
CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
CREATE DATABASE `php-crud-api` CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
|
||||
GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
@@ -2,13 +2,12 @@ FROM debian:9
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# install: php / mysql / postgres / sqlite / tools / mssql deps
|
||||
# install: php / mysql / postgres / tools / mssql deps
|
||||
RUN apt-get update && apt-get -y install \
|
||||
php-cli php-xml php-mbstring \
|
||||
php-cli php-xml \
|
||||
mariadb-server mariadb-client php-mysql \
|
||||
postgresql php-pgsql \
|
||||
postgresql-9.6-postgis-2.3 \
|
||||
sqlite3 php-sqlite3 \
|
||||
git wget
|
||||
|
||||
# install run script
|
||||
|
||||
@@ -14,7 +14,7 @@ while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
|
||||
done
|
||||
# create database and user on mysql
|
||||
mysql -u root >/dev/null << 'EOF'
|
||||
CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
CREATE DATABASE `php-crud-api` CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
|
||||
GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
@@ -4,7 +4,7 @@ ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# install: php / mysql / postgres / tools / mssql deps
|
||||
RUN apt-get update && apt-get -y install \
|
||||
php-cli php-xml php-mbstring \
|
||||
php-cli php-xml \
|
||||
mariadb-server mariadb-client php-mysql \
|
||||
postgresql php-pgsql \
|
||||
postgresql-9.5-postgis-2.2 \
|
||||
|
||||
@@ -14,7 +14,7 @@ while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
|
||||
done
|
||||
# create database and user on mysql
|
||||
mysql -u root >/dev/null << 'EOF'
|
||||
CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
CREATE DATABASE `php-crud-api` CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
|
||||
GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;
|
||||
@@ -41,7 +41,7 @@ echo "done"
|
||||
|
||||
echo -n "[3/4] Starting SQLServer 2017 ... "
|
||||
# run sqlserver server
|
||||
nohup /opt/mssql/bin/sqlservr --accept-eula > /root/mssql.log 2>&1 &
|
||||
nohup /opt/mssql/bin/sqlservr --accept-eula > /root/mysql.log 2>&1 &
|
||||
# create database and user on postgres
|
||||
/opt/mssql-tools/bin/sqlcmd -l 30 -S localhost -U SA -P sapwd123! >/dev/null << 'EOF'
|
||||
CREATE DATABASE [php-crud-api]
|
||||
|
||||
@@ -2,13 +2,12 @@ FROM ubuntu:18.04
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# install: php / mysql / postgres / sqlite / tools
|
||||
# install: php / mysql / postgres / tools
|
||||
RUN apt-get update && apt-get -y install \
|
||||
php-cli php-xml php-mbstring \
|
||||
php-cli php-xml \
|
||||
mysql-server mysql-client php-mysql \
|
||||
postgresql php-pgsql \
|
||||
postgresql-10-postgis-2.4 \
|
||||
sqlite3 php-sqlite3 \
|
||||
git wget
|
||||
|
||||
# install locales
|
||||
|
||||
@@ -14,7 +14,7 @@ while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
|
||||
done
|
||||
# create database and user on mysql
|
||||
mysql -u root >/dev/null << 'EOF'
|
||||
CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
CREATE DATABASE `php-crud-api` CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
|
||||
GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# install: php / mysql / postgres / sqlite / tools
|
||||
RUN apt-get update && apt-get -y install \
|
||||
php-cli php-xml php-mbstring \
|
||||
mysql-server mysql-client php-mysql \
|
||||
postgresql php-pgsql \
|
||||
postgresql-12-postgis-3 \
|
||||
sqlite3 php-sqlite3 \
|
||||
git wget
|
||||
|
||||
# install locales
|
||||
RUN apt-get -y install locales
|
||||
RUN locale-gen en_US.UTF-8
|
||||
RUN update-locale LANG=en_US.UTF-8
|
||||
|
||||
# install run script
|
||||
ADD run.sh /usr/sbin/docker-run
|
||||
CMD docker-run
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "================================================"
|
||||
echo " Ubuntu 20.04 (PHP 7.4)"
|
||||
echo "================================================"
|
||||
|
||||
echo -n "[1/4] Starting MySQL 8.0 ........ "
|
||||
# make sure mysql can create socket and lock
|
||||
mkdir /var/run/mysqld && chmod 777 /var/run/mysqld
|
||||
# run mysql server
|
||||
nohup mysqld > /root/mysql.log 2>&1 &
|
||||
# wait for mysql to become available
|
||||
while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
|
||||
sleep 1
|
||||
done
|
||||
# create database and user on mysql
|
||||
mysql -u root >/dev/null << 'EOF'
|
||||
CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
CREATE USER 'php-crud-api'@'localhost' IDENTIFIED WITH MYSQL_NATIVE_PASSWORD BY 'php-crud-api';
|
||||
GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
echo "done"
|
||||
|
||||
echo -n "[2/4] Starting PostgreSQL 12.2 .. "
|
||||
# ensure statistics can be written
|
||||
mkdir /var/run/postgresql/10-main.pg_stat_tmp/ && chmod 777 /var/run/postgresql/10-main.pg_stat_tmp/
|
||||
# run postgres server
|
||||
nohup su - -c "/usr/lib/postgresql/12/bin/postgres -D /etc/postgresql/12/main" postgres > /root/postgres.log 2>&1 &
|
||||
# wait for postgres to become available
|
||||
until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
|
||||
sleep 1;
|
||||
done
|
||||
# create database and user on postgres
|
||||
su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
|
||||
CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
|
||||
CREATE DATABASE "php-crud-api";
|
||||
GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
|
||||
\c "php-crud-api";
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
\q
|
||||
EOF
|
||||
echo "done"
|
||||
|
||||
echo -n "[3/4] Starting SQLServer 2017 ... "
|
||||
echo "skipped"
|
||||
|
||||
echo -n "[4/4] Cloning PHP-CRUD-API v2 ... "
|
||||
# install software
|
||||
if [ -d /php-crud-api ]; then
|
||||
echo "skipped"
|
||||
else
|
||||
git clone --quiet https://github.com/mevdschee/php-crud-api.git
|
||||
echo "done"
|
||||
fi
|
||||
|
||||
echo "------------------------------------------------"
|
||||
|
||||
# run the tests
|
||||
cd php-crud-api
|
||||
php test.php
|
||||
@@ -7,7 +7,7 @@ var clientId = ''; // client id as defined in auth0
|
||||
var audience = 'https://your-php-crud-api/api.php'; // api audience as defined in auth0
|
||||
var url = '/api.php/records/posts?join=categories&join=tags&join=comments&filter=id,eq,1';
|
||||
|
||||
function requestAPI() {
|
||||
function requestApi() {
|
||||
var match = RegExp('[#&]access_token=([^&]*)').exec(window.location.hash);
|
||||
var accessToken = match && decodeURIComponent(match[1].replace(/\+/g, ' '));
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.20/css/jquery.dataTables.min.css"/>
|
||||
<script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.js"></script>
|
||||
<script type="text/javascript" src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#example').DataTable( {
|
||||
ajax: {
|
||||
url: '/api.php/records/posts?join=categories&join=users',
|
||||
dataSrc: 'records'
|
||||
},
|
||||
columns: [
|
||||
{ data: "id" },
|
||||
{ data: "user_id.username" },
|
||||
{ data: "category_id.name" },
|
||||
{ data: "content" }
|
||||
]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<table id="example" class="display" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Category</th>
|
||||
<th>Content</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Category</th>
|
||||
<th>Content</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,6 +6,6 @@ if (!file_exists('composer.phar')) {
|
||||
$composer = file_get_contents('https://getcomposer.org/composer.phar');
|
||||
file_put_contents('composer.phar', $composer);
|
||||
}
|
||||
exec('php composer.phar install --ignore-platform-reqs');
|
||||
exec('php composer.phar install');
|
||||
|
||||
include 'patch.php';
|
||||
|
||||
@@ -23,7 +23,6 @@ function patchDir(string $base, string $dir): int
|
||||
}
|
||||
$patched = $original = file_get_contents($filename);
|
||||
$patched = preg_replace('/\):\s*(\?[a-zA-Z]+|void)\s*\n/', ") /*:$1*/\n", $patched);
|
||||
$patched = preg_replace('/([\(,])\s*(\?[a-zA-Z]+|void)\s+\$/', "$1 /*$2*/ \$", $patched);
|
||||
$patched = preg_replace('/(private|public|protected) const/', "/*$1*/ const", $patched);
|
||||
if ($patched && $patched != $original) {
|
||||
file_put_contents($filename, $patched);
|
||||
|
||||
@@ -30,9 +30,7 @@ use Tqdev\PhpCrudApi\Middleware\PageLimitsMiddleware;
|
||||
use Tqdev\PhpCrudApi\Middleware\ReconnectMiddleware;
|
||||
use Tqdev\PhpCrudApi\Middleware\Router\SimpleRouter;
|
||||
use Tqdev\PhpCrudApi\Middleware\SanitationMiddleware;
|
||||
use Tqdev\PhpCrudApi\Middleware\SslRedirectMiddleware;
|
||||
use Tqdev\PhpCrudApi\Middleware\ValidationMiddleware;
|
||||
use Tqdev\PhpCrudApi\Middleware\XmlMiddleware;
|
||||
use Tqdev\PhpCrudApi\Middleware\XsrfMiddleware;
|
||||
use Tqdev\PhpCrudApi\OpenApi\OpenApiService;
|
||||
use Tqdev\PhpCrudApi\Record\ErrorCode;
|
||||
@@ -63,11 +61,8 @@ class Api implements RequestHandlerInterface
|
||||
$router = new SimpleRouter($config->getBasePath(), $responder, $cache, $config->getCacheTime(), $config->getDebug());
|
||||
foreach ($config->getMiddlewares() as $middleware => $properties) {
|
||||
switch ($middleware) {
|
||||
case 'sslRedirect':
|
||||
new SslRedirectMiddleware($router, $responder, $properties);
|
||||
break;
|
||||
case 'cors':
|
||||
new CorsMiddleware($router, $responder, $properties, $config->getDebug());
|
||||
new CorsMiddleware($router, $responder, $properties);
|
||||
break;
|
||||
case 'firewall':
|
||||
new FirewallMiddleware($router, $responder, $properties);
|
||||
@@ -111,9 +106,6 @@ class Api implements RequestHandlerInterface
|
||||
case 'customization':
|
||||
new CustomizationMiddleware($router, $responder, $properties, $reflection);
|
||||
break;
|
||||
case 'xml':
|
||||
new XmlMiddleware($router, $responder, $properties, $reflection);
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach ($config->getControllers() as $controller) {
|
||||
@@ -210,6 +202,15 @@ class Api implements RequestHandlerInterface
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return $this->router->route($this->addParsedBody($request));
|
||||
$response = null;
|
||||
try {
|
||||
$response = $this->router->route($this->addParsedBody($request));
|
||||
} catch (\Throwable $e) {
|
||||
$response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
|
||||
if ($this->debug) {
|
||||
$response = ResponseUtils::addExceptionHeaders($response, $e);
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +86,6 @@ class TempFileCache implements Cache
|
||||
if ($data === false) {
|
||||
return '';
|
||||
}
|
||||
if (strpos($data, '|') === false) {
|
||||
return '';
|
||||
}
|
||||
list($ttl, $string) = explode('|', $data, 2);
|
||||
if ($ttl > 0 && time() - filemtime($filename) > $ttl) {
|
||||
return '';
|
||||
@@ -121,18 +118,18 @@ class TempFileCache implements Cache
|
||||
if (strlen($entry) != $len) {
|
||||
continue;
|
||||
}
|
||||
if (file_exists($filename) && is_file($filename)) {
|
||||
if (is_file($filename)) {
|
||||
if ($all || $this->getString($filename) == null) {
|
||||
@unlink($filename);
|
||||
unlink($filename);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (strlen($entry) != $segments[0]) {
|
||||
continue;
|
||||
}
|
||||
if (file_exists($filename) && is_dir($filename)) {
|
||||
if (is_dir($filename)) {
|
||||
$this->clean($filename, array_slice($segments, 1), $len - $segments[0], $all);
|
||||
@rmdir($filename);
|
||||
rmdir($filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,7 @@ class DefinitionService
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (
|
||||
$newColumn->getType() != $column->getType() ||
|
||||
if ($newColumn->getType() != $column->getType() ||
|
||||
$newColumn->getLength() != $column->getLength() ||
|
||||
$newColumn->getPrecision() != $column->getPrecision() ||
|
||||
$newColumn->getScale() != $column->getScale()
|
||||
|
||||
@@ -49,43 +49,23 @@ class ReflectedTable implements \JsonSerializable
|
||||
$columns[$column->getName()] = $column;
|
||||
}
|
||||
// set primary key
|
||||
$columnName = false;
|
||||
if ($type == 'view') {
|
||||
$columnName = 'id';
|
||||
} else {
|
||||
$columnNames = $reflection->getTablePrimaryKeys($name);
|
||||
if (count($columnNames) == 1) {
|
||||
$columnName = $columnNames[0];
|
||||
$columnNames = $reflection->getTablePrimaryKeys($name);
|
||||
if (count($columnNames) == 1) {
|
||||
$columnName = $columnNames[0];
|
||||
if (isset($columns[$columnName])) {
|
||||
$pk = $columns[$columnName];
|
||||
$pk->setPk(true);
|
||||
}
|
||||
}
|
||||
if ($columnName && isset($columns[$columnName])) {
|
||||
$pk = $columns[$columnName];
|
||||
$pk->setPk(true);
|
||||
}
|
||||
// set foreign keys
|
||||
if ($type == 'view') {
|
||||
$tables = $reflection->getTables();
|
||||
foreach ($columns as $columnName => $column) {
|
||||
if (substr($columnName, -3) == '_id') {
|
||||
foreach ($tables as $table) {
|
||||
$tableName = $table['TABLE_NAME'];
|
||||
$suffix = $tableName . '_id';
|
||||
if (substr($columnName, -1 * strlen($suffix)) == $suffix) {
|
||||
$column->setFk($tableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$fks = $reflection->getTableForeignKeys($name);
|
||||
foreach ($fks as $columnName => $table) {
|
||||
$columns[$columnName]->setFk($table);
|
||||
}
|
||||
$fks = $reflection->getTableForeignKeys($name);
|
||||
foreach ($fks as $columnName => $table) {
|
||||
$columns[$columnName]->setFk($table);
|
||||
}
|
||||
return new ReflectedTable($name, $type, array_values($columns));
|
||||
}
|
||||
|
||||
public static function fromJson( /* object */$json): ReflectedTable
|
||||
public static function fromJson(/* object */$json): ReflectedTable
|
||||
{
|
||||
$name = $json->name;
|
||||
$type = isset($json->type) ? $json->type : 'table';
|
||||
@@ -137,7 +117,7 @@ class ReflectedTable implements \JsonSerializable
|
||||
{
|
||||
$columns = array();
|
||||
foreach ($this->fks as $columnName => $referencedTableName) {
|
||||
if ($tableName == $referencedTableName && !is_null($this->columns[$columnName])) {
|
||||
if ($tableName == $referencedTableName) {
|
||||
$columns[] = $this->columns[$columnName];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,11 @@ class ReflectionService
|
||||
return $this->database()->getTableNames();
|
||||
}
|
||||
|
||||
public function getDatabaseName(): string
|
||||
{
|
||||
return $this->database()->getName();
|
||||
}
|
||||
|
||||
public function removeTable(string $tableName): bool
|
||||
{
|
||||
unset($this->tables[$tableName]);
|
||||
|
||||
@@ -12,7 +12,7 @@ class Config
|
||||
'password' => null,
|
||||
'database' => null,
|
||||
'tables' => '',
|
||||
'middlewares' => 'cors,errors',
|
||||
'middlewares' => 'cors',
|
||||
'controllers' => 'records,geojson,openapi',
|
||||
'customControllers' => '',
|
||||
'customOpenApiBuilders' => '',
|
||||
@@ -41,8 +41,6 @@ class Config
|
||||
return 5432;
|
||||
case 'sqlsrv':
|
||||
return 1433;
|
||||
case 'sqlite':
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +53,6 @@ class Config
|
||||
return 'localhost';
|
||||
case 'sqlsrv':
|
||||
return 'localhost';
|
||||
case 'sqlite':
|
||||
return 'data.db';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,16 +65,6 @@ class Config
|
||||
];
|
||||
}
|
||||
|
||||
private function applyEnvironmentVariables(array $values): array
|
||||
{
|
||||
$newValues = array();
|
||||
foreach ($values as $key => $value) {
|
||||
$environmentKey = 'PHP_CRUD_API_' . strtoupper(preg_replace('/(?<!^)[A-Z]/', '_$0', str_replace('.', '_', $key)));
|
||||
$newValues[$key] = getenv($environmentKey, true) ?: $value;
|
||||
}
|
||||
return $newValues;
|
||||
}
|
||||
|
||||
public function __construct(array $values)
|
||||
{
|
||||
$driver = $this->getDefaultDriver($values);
|
||||
@@ -90,7 +76,6 @@ class Config
|
||||
$key = array_keys($diff)[0];
|
||||
throw new \Exception("Config has invalid value '$key'");
|
||||
}
|
||||
$newValues = $this->applyEnvironmentVariables($newValues);
|
||||
$this->values = $newValues;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ class RecordController
|
||||
if (!$this->service->hasTable($table)) {
|
||||
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
|
||||
}
|
||||
if ($this->service->getType($table) != 'table') {
|
||||
return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
|
||||
}
|
||||
$id = RequestUtils::getPathSegment($request, 3);
|
||||
$params = RequestUtils::getParams($request);
|
||||
if (strpos($id, ',') !== false) {
|
||||
|
||||
@@ -22,14 +22,9 @@ class ColumnsBuilder
|
||||
return '';
|
||||
}
|
||||
switch ($this->driver) {
|
||||
case 'mysql':
|
||||
return " LIMIT $offset, $limit";
|
||||
case 'pgsql':
|
||||
return " LIMIT $limit OFFSET $offset";
|
||||
case 'sqlsrv':
|
||||
return " OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY";
|
||||
case 'sqlite':
|
||||
return " LIMIT $limit OFFSET $offset";
|
||||
case 'mysql':return " LIMIT $offset, $limit";
|
||||
case 'pgsql':return " LIMIT $limit OFFSET $offset";
|
||||
case 'sqlsrv':return " OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,14 +74,9 @@ class ColumnsBuilder
|
||||
$valuesSql = '(' . implode(',', $values) . ')';
|
||||
$outputColumn = $this->quoteColumnName($table->getPk());
|
||||
switch ($this->driver) {
|
||||
case 'mysql':
|
||||
return "$columnsSql VALUES $valuesSql";
|
||||
case 'pgsql':
|
||||
return "$columnsSql VALUES $valuesSql RETURNING $outputColumn";
|
||||
case 'sqlsrv':
|
||||
return "$columnsSql OUTPUT INSERTED.$outputColumn VALUES $valuesSql";
|
||||
case 'sqlite':
|
||||
return "$columnsSql VALUES $valuesSql";
|
||||
case 'mysql':return "$columnsSql VALUES $valuesSql";
|
||||
case 'pgsql':return "$columnsSql VALUES $valuesSql RETURNING $outputColumn";
|
||||
case 'sqlsrv':return "$columnsSql OUTPUT INSERTED.$outputColumn VALUES $valuesSql";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -146,28 +146,17 @@ class ConditionsBuilder
|
||||
private function getSpatialFunctionName(string $operator): string
|
||||
{
|
||||
switch ($operator) {
|
||||
case 'co':
|
||||
return 'ST_Contains';
|
||||
case 'cr':
|
||||
return 'ST_Crosses';
|
||||
case 'di':
|
||||
return 'ST_Disjoint';
|
||||
case 'eq':
|
||||
return 'ST_Equals';
|
||||
case 'in':
|
||||
return 'ST_Intersects';
|
||||
case 'ov':
|
||||
return 'ST_Overlaps';
|
||||
case 'to':
|
||||
return 'ST_Touches';
|
||||
case 'wi':
|
||||
return 'ST_Within';
|
||||
case 'ic':
|
||||
return 'ST_IsClosed';
|
||||
case 'is':
|
||||
return 'ST_IsSimple';
|
||||
case 'iv':
|
||||
return 'ST_IsValid';
|
||||
case 'co':return 'ST_Contains';
|
||||
case 'cr':return 'ST_Crosses';
|
||||
case 'di':return 'ST_Disjoint';
|
||||
case 'eq':return 'ST_Equals';
|
||||
case 'in':return 'ST_Intersects';
|
||||
case 'ov':return 'ST_Overlaps';
|
||||
case 'to':return 'ST_Touches';
|
||||
case 'wi':return 'ST_Within';
|
||||
case 'ic':return 'ST_IsClosed';
|
||||
case 'is':return 'ST_IsSimple';
|
||||
case 'iv':return 'ST_IsValid';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,9 +176,6 @@ class ConditionsBuilder
|
||||
$functionName = str_replace('ST_', 'ST', $functionName);
|
||||
$argument = $hasArgument ? 'geometry::STGeomFromText(?,0)' : '';
|
||||
return "$column.$functionName($argument)=1";
|
||||
case 'sqlite':
|
||||
$argument = $hasArgument ? '?' : '0';
|
||||
return "$functionName($column, $argument)=1";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,35 +16,23 @@ class DataConverter
|
||||
|
||||
private function convertRecordValue($conversion, $value)
|
||||
{
|
||||
$args = explode('|', $conversion);
|
||||
$type = array_shift($args);
|
||||
switch ($type) {
|
||||
switch ($conversion) {
|
||||
case 'boolean':
|
||||
return $value ? true : false;
|
||||
case 'integer':
|
||||
return (int) $value;
|
||||
case 'float':
|
||||
return (float) $value;
|
||||
case 'decimal':
|
||||
return number_format($value, $args[0], '.', '');
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function getRecordValueConversion(ReflectedColumn $column): string
|
||||
{
|
||||
if (in_array($this->driver, ['mysql', 'sqlsrv', 'sqlite']) && $column->isBoolean()) {
|
||||
if (in_array($this->driver, ['mysql', 'sqlsrv']) && $column->isBoolean()) {
|
||||
return 'boolean';
|
||||
}
|
||||
if (in_array($this->driver, ['sqlsrv', 'sqlite']) && in_array($column->getType(), ['integer', 'bigint'])) {
|
||||
if ($this->driver == 'sqlsrv' && $column->getType() == 'bigint') {
|
||||
return 'integer';
|
||||
}
|
||||
if (in_array($this->driver, ['sqlite', 'pgsql']) && in_array($column->getType(), ['float', 'double'])) {
|
||||
return 'float';
|
||||
}
|
||||
if (in_array($this->driver, ['sqlite']) && in_array($column->getType(), ['decimal'])) {
|
||||
return 'decimal|' . $column->getScale();
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ class GenericDB
|
||||
return "$this->driver:host=$this->address port=$this->port dbname=$this->database options='--client_encoding=UTF8'";
|
||||
case 'sqlsrv':
|
||||
return "$this->driver:Server=$this->address,$this->port;Database=$this->database";
|
||||
case 'sqlite':
|
||||
return "$this->driver:$this->address";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,10 +50,6 @@ class GenericDB
|
||||
];
|
||||
case 'sqlsrv':
|
||||
return [];
|
||||
case 'sqlite':
|
||||
return [
|
||||
'PRAGMA foreign_keys = on;',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +76,6 @@ class GenericDB
|
||||
\PDO::SQLSRV_ATTR_DIRECT_QUERY => false,
|
||||
\PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
|
||||
];
|
||||
case 'sqlite':
|
||||
return $options + [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,17 +183,11 @@ class GenericDB
|
||||
case 'mysql':
|
||||
$stmt = $this->query('SELECT LAST_INSERT_ID()', []);
|
||||
break;
|
||||
case 'sqlite':
|
||||
$stmt = $this->query('SELECT LAST_INSERT_ROWID()', []);
|
||||
break;
|
||||
}
|
||||
$pkValue = $stmt->fetchColumn(0);
|
||||
if ($this->driver == 'sqlsrv' && $table->getPk()->getType() == 'bigint') {
|
||||
return (int) $pkValue;
|
||||
}
|
||||
if ($this->driver == 'sqlite' && in_array($table->getPk()->getType(), ['integer', 'bigint'])) {
|
||||
return (int) $pkValue;
|
||||
}
|
||||
return $pkValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,9 +72,7 @@ class GenericDefinition
|
||||
return $column->getPk() ? ' AUTO_INCREMENT' : '';
|
||||
case 'pgsql':
|
||||
case 'sqlsrv':
|
||||
return $column->getPk() ? ' IDENTITY(1,1)' : '';
|
||||
case 'sqlite':
|
||||
return $column->getPk() ? ' AUTOINCREMENT' : '';
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +96,6 @@ class GenericDefinition
|
||||
return "ALTER TABLE $p1 RENAME TO $p2";
|
||||
case 'sqlsrv':
|
||||
return "EXEC sp_rename $p1, $p2";
|
||||
case 'sqlite':
|
||||
return "ALTER TABLE $p1 RENAME TO $p2";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +114,6 @@ class GenericDefinition
|
||||
case 'sqlsrv':
|
||||
$p4 = $this->quote($tableName . '.' . $columnName);
|
||||
return "EXEC sp_rename $p4, $p3, 'COLUMN'";
|
||||
case 'sqlite':
|
||||
return "ALTER TABLE $p1 RENAME COLUMN $p2 TO $p3";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,22 +272,12 @@ class GenericDefinition
|
||||
$f4 = $this->quote($newColumn->getFk());
|
||||
$f5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
|
||||
$f6 = $this->quote($tableName . '_' . $pkColumn . '_pkey');
|
||||
if ($this->driver == 'sqlite') {
|
||||
if ($newColumn->getPk()) {
|
||||
$f2 = str_replace('NULL', 'NULL PRIMARY KEY', $f2);
|
||||
}
|
||||
$fields[] = "$f1 $f2";
|
||||
if ($newColumn->getFk()) {
|
||||
$constraints[] = "FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
|
||||
}
|
||||
} else {
|
||||
$fields[] = "$f1 $f2";
|
||||
if ($newColumn->getPk()) {
|
||||
$constraints[] = "CONSTRAINT $f6 PRIMARY KEY ($f1)";
|
||||
}
|
||||
if ($newColumn->getFk()) {
|
||||
$constraints[] = "CONSTRAINT $f3 FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
|
||||
}
|
||||
$fields[] = "$f1 $f2";
|
||||
if ($newColumn->getPk()) {
|
||||
$constraints[] = "CONSTRAINT $f6 PRIMARY KEY ($f1)";
|
||||
}
|
||||
if ($newColumn->getFk()) {
|
||||
$constraints[] = "CONSTRAINT $f3 FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
|
||||
}
|
||||
}
|
||||
$p2 = implode(',', array_merge($fields, $constraints));
|
||||
@@ -313,8 +297,6 @@ class GenericDefinition
|
||||
return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
|
||||
case 'sqlsrv':
|
||||
return "ALTER TABLE $p1 ADD $p2 $p3";
|
||||
case 'sqlite':
|
||||
return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,8 +310,6 @@ class GenericDefinition
|
||||
return "DROP TABLE $p1 CASCADE;";
|
||||
case 'sqlsrv':
|
||||
return "DROP TABLE $p1;";
|
||||
case 'sqlite':
|
||||
return "DROP TABLE $p1;";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,46 +324,44 @@ class GenericDefinition
|
||||
return "ALTER TABLE $p1 DROP COLUMN $p2 CASCADE;";
|
||||
case 'sqlsrv':
|
||||
return "ALTER TABLE $p1 DROP COLUMN $p2;";
|
||||
case 'sqlite':
|
||||
return "ALTER TABLE $p1 DROP COLUMN $p2;";
|
||||
}
|
||||
}
|
||||
|
||||
public function renameTable(string $tableName, string $newTableName)
|
||||
{
|
||||
$sql = $this->getTableRenameSQL($tableName, $newTableName);
|
||||
return $this->query($sql, []);
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
public function renameColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
|
||||
{
|
||||
$sql = $this->getColumnRenameSQL($tableName, $columnName, $newColumn);
|
||||
return $this->query($sql, []);
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
public function retypeColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
|
||||
{
|
||||
$sql = $this->getColumnRetypeSQL($tableName, $columnName, $newColumn);
|
||||
return $this->query($sql, []);
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
public function setColumnNullable(string $tableName, string $columnName, ReflectedColumn $newColumn)
|
||||
{
|
||||
$sql = $this->getSetColumnNullableSQL($tableName, $columnName, $newColumn);
|
||||
return $this->query($sql, []);
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
public function addColumnPrimaryKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
|
||||
{
|
||||
$sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
|
||||
$this->query($sql, []);
|
||||
$this->query($sql);
|
||||
if ($this->canAutoIncrement($newColumn)) {
|
||||
$sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
|
||||
$this->query($sql, []);
|
||||
$this->query($sql);
|
||||
$sql = $this->getSetColumnPkSequenceStartSQL($tableName, $columnName, $newColumn);
|
||||
$this->query($sql, []);
|
||||
$this->query($sql);
|
||||
$sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
|
||||
$this->query($sql, []);
|
||||
$this->query($sql);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -392,55 +370,55 @@ class GenericDefinition
|
||||
{
|
||||
if ($this->canAutoIncrement($newColumn)) {
|
||||
$sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
|
||||
$this->query($sql, []);
|
||||
$this->query($sql);
|
||||
$sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
|
||||
$this->query($sql, []);
|
||||
$this->query($sql);
|
||||
}
|
||||
$sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
|
||||
$this->query($sql, []);
|
||||
$this->query($sql);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function addColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
|
||||
{
|
||||
$sql = $this->getAddColumnFkConstraintSQL($tableName, $columnName, $newColumn);
|
||||
return $this->query($sql, []);
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
public function removeColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
|
||||
{
|
||||
$sql = $this->getRemoveColumnFkConstraintSQL($tableName, $columnName, $newColumn);
|
||||
return $this->query($sql, []);
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
public function addTable(ReflectedTable $newTable)
|
||||
{
|
||||
$sql = $this->getAddTableSQL($newTable);
|
||||
return $this->query($sql, []);
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
public function addColumn(string $tableName, ReflectedColumn $newColumn)
|
||||
{
|
||||
$sql = $this->getAddColumnSQL($tableName, $newColumn);
|
||||
return $this->query($sql, []);
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
public function removeTable(string $tableName)
|
||||
{
|
||||
$sql = $this->getRemoveTableSQL($tableName);
|
||||
return $this->query($sql, []);
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
public function removeColumn(string $tableName, string $columnName)
|
||||
{
|
||||
$sql = $this->getRemoveColumnSQL($tableName, $columnName);
|
||||
return $this->query($sql, []);
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
private function query(string $sql, array $arguments): bool
|
||||
private function query(string $sql): bool
|
||||
{
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
// echo "- $sql -- " . json_encode($arguments) . "\n";
|
||||
return $stmt->execute($arguments);
|
||||
//echo "- $sql -- []\n";
|
||||
return $stmt->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ class GenericReflection
|
||||
return ['spatial_ref_sys', 'raster_columns', 'raster_overviews', 'geography_columns', 'geometry_columns'];
|
||||
case 'sqlsrv':
|
||||
return [];
|
||||
case 'sqlite':
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +42,6 @@ class GenericReflection
|
||||
return 'SELECT c.relname as "TABLE_NAME", c.relkind as "TABLE_TYPE" FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN (\'r\', \'v\') AND n.nspname <> \'pg_catalog\' AND n.nspname <> \'information_schema\' AND n.nspname !~ \'^pg_toast\' AND pg_catalog.pg_table_is_visible(c.oid) AND \'\' <> ? ORDER BY "TABLE_NAME";';
|
||||
case 'sqlsrv':
|
||||
return 'SELECT o.name as "TABLE_NAME", o.xtype as "TABLE_TYPE" FROM sysobjects o WHERE o.xtype IN (\'U\', \'V\') ORDER BY "TABLE_NAME"';
|
||||
case 'sqlite':
|
||||
return 'SELECT t.name as "TABLE_NAME", t.type as "TABLE_TYPE" FROM sqlite_master t WHERE t.type IN (\'table\', \'view\') AND \'\' <> ? ORDER BY "TABLE_NAME"';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,13 +49,11 @@ class GenericReflection
|
||||
{
|
||||
switch ($this->driver) {
|
||||
case 'mysql':
|
||||
return 'SELECT "COLUMN_NAME", "IS_NULLABLE", "DATA_TYPE", "CHARACTER_MAXIMUM_LENGTH" as "CHARACTER_MAXIMUM_LENGTH", "NUMERIC_PRECISION", "NUMERIC_SCALE", "COLUMN_TYPE" FROM "INFORMATION_SCHEMA"."COLUMNS" WHERE "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ? ORDER BY "ORDINAL_POSITION"';
|
||||
return 'SELECT "COLUMN_NAME", "IS_NULLABLE", "DATA_TYPE", "CHARACTER_MAXIMUM_LENGTH" as "CHARACTER_MAXIMUM_LENGTH", "NUMERIC_PRECISION", "NUMERIC_SCALE", "COLUMN_TYPE" FROM "INFORMATION_SCHEMA"."COLUMNS" WHERE "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
|
||||
case 'pgsql':
|
||||
return 'SELECT a.attname AS "COLUMN_NAME", case when a.attnotnull then \'NO\' else \'YES\' end as "IS_NULLABLE", pg_catalog.format_type(a.atttypid, -1) as "DATA_TYPE", case when a.atttypmod < 0 then NULL else a.atttypmod-4 end as "CHARACTER_MAXIMUM_LENGTH", case when a.atttypid != 1700 then NULL else ((a.atttypmod - 4) >> 16) & 65535 end as "NUMERIC_PRECISION", case when a.atttypid != 1700 then NULL else (a.atttypmod - 4) & 65535 end as "NUMERIC_SCALE", \'\' AS "COLUMN_TYPE" FROM pg_attribute a JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum;';
|
||||
return 'SELECT a.attname AS "COLUMN_NAME", case when a.attnotnull then \'NO\' else \'YES\' end as "IS_NULLABLE", pg_catalog.format_type(a.atttypid, -1) as "DATA_TYPE", case when a.atttypmod < 0 then NULL else a.atttypmod-4 end as "CHARACTER_MAXIMUM_LENGTH", case when a.atttypid != 1700 then NULL else ((a.atttypmod - 4) >> 16) & 65535 end as "NUMERIC_PRECISION", case when a.atttypid != 1700 then NULL else (a.atttypmod - 4) & 65535 end as "NUMERIC_SCALE", \'\' AS "COLUMN_TYPE" FROM pg_attribute a JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND a.attnum > 0 AND NOT a.attisdropped;';
|
||||
case 'sqlsrv':
|
||||
return 'SELECT c.name AS "COLUMN_NAME", c.is_nullable AS "IS_NULLABLE", t.Name AS "DATA_TYPE", (c.max_length/2) AS "CHARACTER_MAXIMUM_LENGTH", c.precision AS "NUMERIC_PRECISION", c.scale AS "NUMERIC_SCALE", \'\' AS "COLUMN_TYPE" FROM sys.columns c INNER JOIN sys.types t ON c.user_type_id = t.user_type_id WHERE c.object_id = OBJECT_ID(?) AND \'\' <> ? ORDER BY c.column_id';
|
||||
case 'sqlite':
|
||||
return 'SELECT "name" AS "COLUMN_NAME", case when "notnull"==1 then \'no\' else \'yes\' end as "IS_NULLABLE", lower("type") AS "DATA_TYPE", 2147483647 AS "CHARACTER_MAXIMUM_LENGTH", 0 AS "NUMERIC_PRECISION", 0 AS "NUMERIC_SCALE", \'\' AS "COLUMN_TYPE" FROM pragma_table_info(?) WHERE \'\' <> ? ORDER BY "cid"';
|
||||
return 'SELECT c.name AS "COLUMN_NAME", c.is_nullable AS "IS_NULLABLE", t.Name AS "DATA_TYPE", (c.max_length/2) AS "CHARACTER_MAXIMUM_LENGTH", c.precision AS "NUMERIC_PRECISION", c.scale AS "NUMERIC_SCALE", \'\' AS "COLUMN_TYPE" FROM sys.columns c INNER JOIN sys.types t ON c.user_type_id = t.user_type_id WHERE c.object_id = OBJECT_ID(?) AND \'\' <> ?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +66,6 @@ class GenericReflection
|
||||
return 'SELECT a.attname AS "COLUMN_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'p\'';
|
||||
case 'sqlsrv':
|
||||
return 'SELECT c.NAME as "COLUMN_NAME" FROM sys.key_constraints kc inner join sys.objects t on t.object_id = kc.parent_object_id INNER JOIN sys.index_columns ic ON kc.parent_object_id = ic.object_id and kc.unique_index_id = ic.index_id INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id WHERE kc.type = \'PK\' and t.object_id = OBJECT_ID(?) and \'\' <> ?';
|
||||
case 'sqlite':
|
||||
return 'SELECT "name" as "COLUMN_NAME" FROM pragma_table_info(?) WHERE "pk"=1 AND \'\' <> ?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +78,6 @@ class GenericReflection
|
||||
return 'SELECT a.attname AS "COLUMN_NAME", c.confrelid::regclass::text AS "REFERENCED_TABLE_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'f\'';
|
||||
case 'sqlsrv':
|
||||
return 'SELECT COL_NAME(fc.parent_object_id, fc.parent_column_id) AS "COLUMN_NAME", OBJECT_NAME (f.referenced_object_id) AS "REFERENCED_TABLE_NAME" FROM sys.foreign_keys AS f INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id WHERE f.parent_object_id = OBJECT_ID(?) and \'\' <> ?';
|
||||
case 'sqlite':
|
||||
return 'SELECT "from" AS "COLUMN_NAME", "table" AS "REFERENCED_TABLE_NAME" FROM pragma_foreign_key_list(?) WHERE \'\' <> ?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,22 +95,20 @@ class GenericReflection
|
||||
return !$tables || in_array($v['TABLE_NAME'], $tables);
|
||||
});
|
||||
foreach ($results as &$result) {
|
||||
$map = [];
|
||||
switch ($this->driver) {
|
||||
case 'mysql':
|
||||
$map = ['BASE TABLE' => 'table', 'VIEW' => 'view'];
|
||||
$result['TABLE_TYPE'] = $map[$result['TABLE_TYPE']];
|
||||
break;
|
||||
case 'pgsql':
|
||||
$map = ['r' => 'table', 'v' => 'view'];
|
||||
$result['TABLE_TYPE'] = $map[$result['TABLE_TYPE']];
|
||||
break;
|
||||
case 'sqlsrv':
|
||||
$map = ['U' => 'table', 'V' => 'view'];
|
||||
break;
|
||||
case 'sqlite':
|
||||
$map = ['table' => 'table', 'view' => 'view'];
|
||||
$result['TABLE_TYPE'] = $map[trim($result['TABLE_TYPE'])];
|
||||
break;
|
||||
}
|
||||
$result['TABLE_TYPE'] = $map[trim($result['TABLE_TYPE'])];
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
@@ -149,23 +137,6 @@ class GenericReflection
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($this->driver == 'sqlite') {
|
||||
foreach ($results as &$result) {
|
||||
// sqlite does not properly reflect display width of types
|
||||
preg_match('|([a-z]+)(\(([0-9]+)(,([0-9]+))?\))?|', $result['DATA_TYPE'], $matches);
|
||||
if (isset($matches[1])) {
|
||||
$result['DATA_TYPE'] = $matches[1];
|
||||
} else {
|
||||
$result['DATA_TYPE'] = 'integer';
|
||||
}
|
||||
if (isset($matches[5])) {
|
||||
$result['NUMERIC_PRECISION'] = $matches[3];
|
||||
$result['NUMERIC_SCALE'] = $matches[5];
|
||||
} else if (isset($matches[3])) {
|
||||
$result['CHARACTER_MAXIMUM_LENGTH'] = $matches[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,26 +14,19 @@ class TypeConverter
|
||||
private $fromJdbc = [
|
||||
'mysql' => [
|
||||
'clob' => 'longtext',
|
||||
'boolean' => 'tinyint(1)',
|
||||
'boolean' => 'tinyint',
|
||||
'blob' => 'longblob',
|
||||
'timestamp' => 'datetime',
|
||||
],
|
||||
'pgsql' => [
|
||||
'clob' => 'text',
|
||||
'blob' => 'bytea',
|
||||
'float' => 'real',
|
||||
'double' => 'double precision',
|
||||
'varbinary' => 'bytea',
|
||||
],
|
||||
'sqlsrv' => [
|
||||
'boolean' => 'bit',
|
||||
'varchar' => 'nvarchar',
|
||||
'clob' => 'ntext',
|
||||
'blob' => 'image',
|
||||
'time' => 'time(0)',
|
||||
'timestamp' => 'datetime2(0)',
|
||||
'double' => 'float',
|
||||
'float' => 'real',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -128,20 +121,6 @@ class TypeConverter
|
||||
'uniqueidentifier' => 'char',
|
||||
'xml' => 'clob',
|
||||
],
|
||||
'sqlite' => [
|
||||
'tinytext' => 'clob',
|
||||
'text' => 'clob',
|
||||
'mediumtext' => 'clob',
|
||||
'longtext' => 'clob',
|
||||
'mediumint' => 'integer',
|
||||
'int' => 'integer',
|
||||
'bigint' => 'bigint',
|
||||
'int2' => 'smallint',
|
||||
'int4' => 'integer',
|
||||
'int8' => 'bigint',
|
||||
'double precision' => 'double',
|
||||
'datetime' => 'timestamp'
|
||||
],
|
||||
];
|
||||
|
||||
// source: https://docs.oracle.com/javase/9/docs/api/java/sql/Types.html
|
||||
@@ -202,8 +181,7 @@ class TypeConverter
|
||||
$jdbcType = $this->toJdbc['simplified'][$jdbcType];
|
||||
}
|
||||
if (!isset($this->valid[$jdbcType])) {
|
||||
//throw new \Exception("Unsupported type '$jdbcType' for driver '$this->driver'");
|
||||
$jdbcType = 'clob';
|
||||
throw new \Exception("Unsupported type '$jdbcType' for driver '$this->driver'");
|
||||
}
|
||||
return $jdbcType;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ class Geometry implements \JsonSerializable
|
||||
$coordinates = preg_replace('|([0-9\-\.]+ )+([0-9\-\.]+)|', '[\1\2]', $coordinates);
|
||||
}
|
||||
$coordinates = str_replace(['(', ')', ', ', ' '], ['[', ']', ',', ','], $coordinates);
|
||||
$json = $coordinates;
|
||||
$coordinates = json_decode($coordinates);
|
||||
if (!$coordinates) {
|
||||
throw new \Exception('Could not decode WKT: ' . $wkt);
|
||||
|
||||
@@ -10,7 +10,6 @@ use Tqdev\PhpCrudApi\Controller\Responder;
|
||||
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
|
||||
use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
|
||||
use Tqdev\PhpCrudApi\Middleware\Router\Router;
|
||||
use Tqdev\PhpCrudApi\Record\ErrorCode;
|
||||
use Tqdev\PhpCrudApi\Record\FilterInfo;
|
||||
use Tqdev\PhpCrudApi\RequestUtils;
|
||||
|
||||
@@ -72,20 +71,9 @@ class AuthorizationMiddleware extends Middleware
|
||||
}
|
||||
}
|
||||
|
||||
private function pathHandler(string $path) /*: bool*/
|
||||
{
|
||||
$pathHandler = $this->getProperty('pathHandler', '');
|
||||
return $pathHandler ? call_user_func($pathHandler, $path) : true;
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
|
||||
{
|
||||
$path = RequestUtils::getPathSegment($request, 1);
|
||||
|
||||
if (!$this->pathHandler($path)) {
|
||||
return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getUri()->getPath());
|
||||
}
|
||||
|
||||
$operation = RequestUtils::getOperation($request);
|
||||
$tableNames = RequestUtils::getTableNames($request, $this->reflection);
|
||||
foreach ($tableNames as $tableName) {
|
||||
|
||||
@@ -7,21 +7,11 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Tqdev\PhpCrudApi\Controller\Responder;
|
||||
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
|
||||
use Tqdev\PhpCrudApi\Middleware\Router\Router;
|
||||
use Tqdev\PhpCrudApi\Record\ErrorCode;
|
||||
use Tqdev\PhpCrudApi\ResponseFactory;
|
||||
use Tqdev\PhpCrudApi\ResponseUtils;
|
||||
|
||||
class CorsMiddleware extends Middleware
|
||||
{
|
||||
private $debug;
|
||||
|
||||
public function __construct(Router $router, Responder $responder, array $properties, bool $debug)
|
||||
{
|
||||
parent::__construct($router, $responder, $properties);
|
||||
$this->debug = $debug;
|
||||
}
|
||||
|
||||
private function isOriginAllowed(string $origin, string $allowedOrigins): bool
|
||||
{
|
||||
$found = false;
|
||||
@@ -46,9 +36,6 @@ class CorsMiddleware extends Middleware
|
||||
} elseif ($method == 'OPTIONS') {
|
||||
$response = ResponseFactory::fromStatus(ResponseFactory::OK);
|
||||
$allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization');
|
||||
if ($this->debug) {
|
||||
$allowHeaders = implode(', ', array_filter([$allowHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File']));
|
||||
}
|
||||
if ($allowHeaders) {
|
||||
$response = $response->withHeader('Access-Control-Allow-Headers', $allowHeaders);
|
||||
}
|
||||
@@ -65,22 +52,11 @@ class CorsMiddleware extends Middleware
|
||||
$response = $response->withHeader('Access-Control-Max-Age', $maxAge);
|
||||
}
|
||||
$exposeHeaders = $this->getProperty('exposeHeaders', '');
|
||||
if ($this->debug) {
|
||||
$exposeHeaders = implode(', ', array_filter([$exposeHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File']));
|
||||
}
|
||||
if ($exposeHeaders) {
|
||||
$response = $response->withHeader('Access-Control-Expose-Headers', $exposeHeaders);
|
||||
}
|
||||
} else {
|
||||
$response = null;
|
||||
try {
|
||||
$response = $next->handle($request);
|
||||
} catch (\Throwable $e) {
|
||||
$response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
|
||||
if ($this->debug) {
|
||||
$response = ResponseUtils::addExceptionHeaders($response, $e);
|
||||
}
|
||||
}
|
||||
$response = $next->handle($request);
|
||||
}
|
||||
if ($origin) {
|
||||
$allowCredentials = $this->getProperty('allowCredentials', 'true');
|
||||
|
||||
@@ -42,19 +42,16 @@ class DbAuthMiddleware extends Middleware
|
||||
}
|
||||
$path = RequestUtils::getPathSegment($request, 1);
|
||||
$method = $request->getMethod();
|
||||
if ($method == 'POST' && in_array($path, ['login', 'register', 'password'])) {
|
||||
if ($method == 'POST' && $path == 'login') {
|
||||
$body = $request->getParsedBody();
|
||||
$username = isset($body->username) ? $body->username : '';
|
||||
$password = isset($body->password) ? $body->password : '';
|
||||
$newPassword = isset($body->newPassword) ? $body->newPassword : '';
|
||||
$tableName = $this->getProperty('usersTable', 'users');
|
||||
$table = $this->reflection->getTable($tableName);
|
||||
$usernameColumnName = $this->getProperty('usernameColumn', 'username');
|
||||
$usernameColumn = $table->getColumn($usernameColumnName);
|
||||
$passwordColumnName = $this->getProperty('passwordColumn', 'password');
|
||||
$passwordLength = $this->getProperty('passwordLength', '12');
|
||||
$pkName = $table->getPk()->getName();
|
||||
$registerUser = $this->getProperty('registerUser', '');
|
||||
$passwordColumn = $table->getColumn($passwordColumnName);
|
||||
$condition = new ColumnCondition($usernameColumn, 'eq', $username);
|
||||
$returnedColumns = $this->getProperty('returnedColumns', '');
|
||||
if (!$returnedColumns) {
|
||||
@@ -62,67 +59,20 @@ class DbAuthMiddleware extends Middleware
|
||||
} else {
|
||||
$columnNames = array_map('trim', explode(',', $returnedColumns));
|
||||
$columnNames[] = $passwordColumnName;
|
||||
$columnNames[] = $pkName;
|
||||
}
|
||||
$columnOrdering = $this->ordering->getDefaultColumnOrdering($table);
|
||||
if ($path == 'register') {
|
||||
if (!$registerUser) {
|
||||
return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
|
||||
}
|
||||
if (strlen($password) < $passwordLength) {
|
||||
return $this->responder->error(ErrorCode::PASSWORD_TOO_SHORT, $passwordLength);
|
||||
}
|
||||
$users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
|
||||
if (!empty($users)) {
|
||||
return $this->responder->error(ErrorCode::USER_ALREADY_EXIST, $username);
|
||||
}
|
||||
$data = json_decode($registerUser, true);
|
||||
$data = is_array($data) ? $data : [];
|
||||
$data[$usernameColumnName] = $username;
|
||||
$data[$passwordColumnName] = password_hash($password, PASSWORD_DEFAULT);
|
||||
$this->db->createSingle($table, $data);
|
||||
$users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
|
||||
foreach ($users as $user) {
|
||||
$users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
|
||||
foreach ($users as $user) {
|
||||
if (password_verify($password, $user[$passwordColumnName]) == 1) {
|
||||
if (!headers_sent()) {
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
unset($user[$passwordColumnName]);
|
||||
$_SESSION['user'] = $user;
|
||||
return $this->responder->success($user);
|
||||
}
|
||||
return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
|
||||
}
|
||||
if ($path == 'login') {
|
||||
$users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
|
||||
foreach ($users as $user) {
|
||||
if (password_verify($password, $user[$passwordColumnName]) == 1) {
|
||||
if (!headers_sent()) {
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
unset($user[$passwordColumnName]);
|
||||
$_SESSION['user'] = $user;
|
||||
return $this->responder->success($user);
|
||||
}
|
||||
}
|
||||
return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
|
||||
}
|
||||
if ($path == 'password') {
|
||||
if ($username != ($_SESSION['user'][$usernameColumnName] ?? '')) {
|
||||
return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
|
||||
}
|
||||
if (strlen($newPassword) < $passwordLength) {
|
||||
return $this->responder->error(ErrorCode::PASSWORD_TOO_SHORT, $passwordLength);
|
||||
}
|
||||
$users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
|
||||
foreach ($users as $user) {
|
||||
if (password_verify($password, $user[$passwordColumnName]) == 1) {
|
||||
if (!headers_sent()) {
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
$data = [$passwordColumnName => password_hash($newPassword, PASSWORD_DEFAULT)];
|
||||
$this->db->updateSingle($table, $data, $user[$pkName]);
|
||||
unset($user[$passwordColumnName]);
|
||||
return $this->responder->success($user);
|
||||
}
|
||||
}
|
||||
return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
|
||||
}
|
||||
return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
|
||||
}
|
||||
if ($method == 'POST' && $path == 'logout') {
|
||||
if (isset($_SESSION['user'])) {
|
||||
@@ -135,12 +85,6 @@ class DbAuthMiddleware extends Middleware
|
||||
}
|
||||
return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
|
||||
}
|
||||
if ($method == 'GET' && $path == 'me') {
|
||||
if (isset($_SESSION['user'])) {
|
||||
return $this->responder->success($_SESSION['user']);
|
||||
}
|
||||
return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
|
||||
}
|
||||
if (!isset($_SESSION['user']) || !$_SESSION['user']) {
|
||||
$authenticationMode = $this->getProperty('mode', 'required');
|
||||
if ($authenticationMode == 'required') {
|
||||
|
||||
@@ -38,7 +38,7 @@ class MultiTenancyMiddleware extends Middleware
|
||||
private function getPairs($handler, string $operation, string $tableName): array
|
||||
{
|
||||
$result = array();
|
||||
$pairs = call_user_func($handler, $operation, $tableName) ?: [];
|
||||
$pairs = call_user_func($handler, $operation, $tableName);
|
||||
$table = $this->reflection->getTable($tableName);
|
||||
foreach ($pairs as $k => $v) {
|
||||
if ($table->hasColumn($k)) {
|
||||
|
||||
@@ -50,9 +50,7 @@ class SimpleRouter implements Router
|
||||
return substr($fullPath, 0, -1 * strlen($path));
|
||||
}
|
||||
}
|
||||
if ('/' . basename(__FILE__) == $fullPath) {
|
||||
return $fullPath;
|
||||
}
|
||||
return $fullPath;
|
||||
}
|
||||
return '/';
|
||||
}
|
||||
@@ -95,7 +93,6 @@ class SimpleRouter implements Router
|
||||
$data = gzcompress(json_encode($this->routes, JSON_UNESCAPED_UNICODE));
|
||||
$this->cache->set('PathTree', $data, $this->ttl);
|
||||
}
|
||||
|
||||
return $this->handle($request);
|
||||
}
|
||||
|
||||
@@ -104,7 +101,7 @@ class SimpleRouter implements Router
|
||||
$method = strtoupper($request->getMethod());
|
||||
$path = array();
|
||||
$segment = $method;
|
||||
for ($i = 1; strlen($segment) > 0; $i++) {
|
||||
for ($i = 1; $segment; $i++) {
|
||||
array_push($path, $segment);
|
||||
$segment = RequestUtils::getPathSegment($request, $i);
|
||||
}
|
||||
@@ -144,8 +141,6 @@ class SimpleRouter implements Router
|
||||
} catch (\PDOException $e) {
|
||||
if (strpos(strtolower($e->getMessage()), 'duplicate') !== false) {
|
||||
$response = $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
|
||||
} elseif (strpos(strtolower($e->getMessage()), 'unique constraint') !== false) {
|
||||
$response = $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
|
||||
} elseif (strpos(strtolower($e->getMessage()), 'default value') !== false) {
|
||||
$response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
|
||||
} elseif (strpos(strtolower($e->getMessage()), 'allow nulls') !== false) {
|
||||
|
||||
@@ -6,7 +6,6 @@ use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
|
||||
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
|
||||
use Tqdev\PhpCrudApi\Column\ReflectionService;
|
||||
use Tqdev\PhpCrudApi\Controller\Responder;
|
||||
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
|
||||
@@ -31,97 +30,11 @@ class SanitationMiddleware extends Middleware
|
||||
if ($table->hasColumn($columnName)) {
|
||||
$column = $table->getColumn($columnName);
|
||||
$value = call_user_func($handler, $operation, $tableName, $column->serialize(), $value);
|
||||
$value = $this->sanitizeType($table, $column, $value);
|
||||
}
|
||||
}
|
||||
return (object) $context;
|
||||
}
|
||||
|
||||
private function sanitizeType(ReflectedTable $table, ReflectedColumn $column, $value)
|
||||
{
|
||||
$tables = $this->getArrayProperty('tables', 'all');
|
||||
$types = $this->getArrayProperty('types', 'all');
|
||||
if (
|
||||
(in_array('all', $tables) || in_array($table->getName(), $tables)) &&
|
||||
(in_array('all', $types) || in_array($column->getType(), $types))
|
||||
) {
|
||||
if (is_null($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
$newValue = null;
|
||||
switch ($column->getType()) {
|
||||
case 'integer':
|
||||
case 'bigint':
|
||||
$newValue = filter_var(trim($value), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
|
||||
break;
|
||||
case 'decimal':
|
||||
$newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
|
||||
if (is_float($newValue)) {
|
||||
$newValue = number_format($newValue, $column->getScale(), '.', '');
|
||||
}
|
||||
break;
|
||||
case 'float':
|
||||
case 'double':
|
||||
$newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
|
||||
break;
|
||||
case 'boolean':
|
||||
$newValue = filter_var(trim($value), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
break;
|
||||
case 'date':
|
||||
$time = strtotime(trim($value));
|
||||
if ($time !== false) {
|
||||
$newValue = date('Y-m-d', $time);
|
||||
}
|
||||
break;
|
||||
case 'time':
|
||||
$time = strtotime(trim($value));
|
||||
if ($time !== false) {
|
||||
$newValue = date('H:i:s', $time);
|
||||
}
|
||||
break;
|
||||
case 'timestamp':
|
||||
$time = strtotime(trim($value));
|
||||
if ($time !== false) {
|
||||
$newValue = date('Y-m-d H:i:s', $time);
|
||||
}
|
||||
break;
|
||||
case 'blob':
|
||||
case 'varbinary':
|
||||
// allow base64url format
|
||||
$newValue = strtr(trim($value), '-_', '+/');
|
||||
break;
|
||||
case 'clob':
|
||||
case 'varchar':
|
||||
$newValue = $value;
|
||||
break;
|
||||
case 'geometry':
|
||||
$newValue = trim($value);
|
||||
break;
|
||||
}
|
||||
if (!is_null($newValue)) {
|
||||
$value = $newValue;
|
||||
}
|
||||
} else {
|
||||
switch ($column->getType()) {
|
||||
case 'integer':
|
||||
case 'bigint':
|
||||
if (is_float($value)) {
|
||||
$value = (int) round($value);
|
||||
}
|
||||
break;
|
||||
case 'decimal':
|
||||
if (is_float($value) || is_int($value)) {
|
||||
$value = number_format((float) $value, $column->getScale(), '.', '');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// post process
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
|
||||
{
|
||||
$operation = RequestUtils::getOperation($request);
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tqdev\PhpCrudApi\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
|
||||
use Tqdev\PhpCrudApi\ResponseFactory;
|
||||
|
||||
class SslRedirectMiddleware extends Middleware
|
||||
{
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
|
||||
{
|
||||
$uri = $request->getUri();
|
||||
$scheme = $uri->getScheme();
|
||||
if ($scheme == 'http') {
|
||||
$uri = $request->getUri();
|
||||
$uri = $uri->withScheme('https');
|
||||
$response = ResponseFactory::fromStatus(301);
|
||||
$response = $response->withHeader('Location', $uri->__toString());
|
||||
} else {
|
||||
$response = $next->handle($request);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,8 @@ namespace Tqdev\PhpCrudApi\Middleware;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Tqdev\PhpCrudApi\Column\ReflectionService;
|
||||
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
|
||||
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
|
||||
use Tqdev\PhpCrudApi\Column\ReflectionService;
|
||||
use Tqdev\PhpCrudApi\Controller\Responder;
|
||||
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
|
||||
use Tqdev\PhpCrudApi\Middleware\Router\Router;
|
||||
@@ -16,202 +15,62 @@ use Tqdev\PhpCrudApi\RequestUtils;
|
||||
|
||||
class ValidationMiddleware extends Middleware
|
||||
{
|
||||
private $reflection;
|
||||
private $reflection;
|
||||
|
||||
public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
|
||||
{
|
||||
parent::__construct($router, $responder, $properties);
|
||||
$this->reflection = $reflection;
|
||||
}
|
||||
public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
|
||||
{
|
||||
parent::__construct($router, $responder, $properties);
|
||||
$this->reflection = $reflection;
|
||||
}
|
||||
|
||||
private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: ResponseInterface?*/
|
||||
{
|
||||
$context = (array) $record;
|
||||
$details = array();
|
||||
$tableName = $table->getName();
|
||||
foreach ($context as $columnName => $value) {
|
||||
if ($table->hasColumn($columnName)) {
|
||||
$column = $table->getColumn($columnName);
|
||||
$valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
|
||||
if ($valid === true || $valid === '') {
|
||||
$valid = $this->validateType($table, $column, $value);
|
||||
}
|
||||
if ($valid !== true && $valid !== '') {
|
||||
$details[$columnName] = $valid;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count($details) > 0) {
|
||||
return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: ResponseInterface?*/
|
||||
{
|
||||
$context = (array) $record;
|
||||
$details = array();
|
||||
$tableName = $table->getName();
|
||||
foreach ($context as $columnName => $value) {
|
||||
if ($table->hasColumn($columnName)) {
|
||||
$column = $table->getColumn($columnName);
|
||||
$valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
|
||||
if ($valid !== true && $valid !== '') {
|
||||
$details[$columnName] = $valid;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count($details) > 0) {
|
||||
return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function validateType(ReflectedTable $table, ReflectedColumn $column, $value)
|
||||
{
|
||||
$tables = $this->getArrayProperty('tables', 'all');
|
||||
$types = $this->getArrayProperty('types', 'all');
|
||||
if (
|
||||
(in_array('all', $tables) || in_array($table->getName(), $tables)) &&
|
||||
(in_array('all', $types) || in_array($column->getType(), $types))
|
||||
) {
|
||||
if (is_null($value)) {
|
||||
return ($column->getNullable() ? true : "cannot be null");
|
||||
}
|
||||
if (is_string($value)) {
|
||||
// check for whitespace
|
||||
switch ($column->getType()) {
|
||||
case 'varchar':
|
||||
case 'clob':
|
||||
break;
|
||||
default:
|
||||
if (strlen(trim($value)) != strlen($value)) {
|
||||
return 'illegal whitespace';
|
||||
}
|
||||
break;
|
||||
}
|
||||
// try to parse
|
||||
switch ($column->getType()) {
|
||||
case 'integer':
|
||||
case 'bigint':
|
||||
if (
|
||||
filter_var($value, FILTER_SANITIZE_NUMBER_INT) !== $value ||
|
||||
filter_var($value, FILTER_VALIDATE_INT) === false
|
||||
) {
|
||||
return 'invalid integer';
|
||||
}
|
||||
break;
|
||||
case 'decimal':
|
||||
if (strpos($value, '.') !== false) {
|
||||
list($whole, $decimals) = explode('.', ltrim($value, '-'), 2);
|
||||
} else {
|
||||
list($whole, $decimals) = array(ltrim($value, '-'), '');
|
||||
}
|
||||
if (strlen($whole) > 0 && !ctype_digit($whole)) {
|
||||
return 'invalid decimal';
|
||||
}
|
||||
if (strlen($decimals) > 0 && !ctype_digit($decimals)) {
|
||||
return 'invalid decimal';
|
||||
}
|
||||
if (strlen($whole) > $column->getPrecision() - $column->getScale()) {
|
||||
return 'decimal too large';
|
||||
}
|
||||
if (strlen($decimals) > $column->getScale()) {
|
||||
return 'decimal too precise';
|
||||
}
|
||||
break;
|
||||
case 'float':
|
||||
case 'double':
|
||||
if (
|
||||
filter_var($value, FILTER_SANITIZE_NUMBER_FLOAT) !== $value ||
|
||||
filter_var($value, FILTER_VALIDATE_FLOAT) === false
|
||||
) {
|
||||
return 'invalid float';
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
if (!in_array(strtolower($value), array('true', 'false'))) {
|
||||
return 'invalid boolean';
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
if (date_create_from_format('Y-m-d', $value) === false) {
|
||||
return 'invalid date';
|
||||
}
|
||||
break;
|
||||
case 'time':
|
||||
if (date_create_from_format('H:i:s', $value) === false) {
|
||||
return 'invalid time';
|
||||
}
|
||||
break;
|
||||
case 'timestamp':
|
||||
if (date_create_from_format('Y-m-d H:i:s', $value) === false) {
|
||||
return 'invalid timestamp';
|
||||
}
|
||||
break;
|
||||
case 'clob':
|
||||
case 'varchar':
|
||||
if ($column->hasLength() && mb_strlen($value, 'UTF-8') > $column->getLength()) {
|
||||
return 'string too long';
|
||||
}
|
||||
break;
|
||||
case 'blob':
|
||||
case 'varbinary':
|
||||
if (base64_decode($value, true) === false) {
|
||||
return 'invalid base64';
|
||||
}
|
||||
if ($column->hasLength() && strlen(base64_decode($value)) > $column->getLength()) {
|
||||
return 'string too long';
|
||||
}
|
||||
break;
|
||||
case 'geometry':
|
||||
// no checks yet
|
||||
break;
|
||||
}
|
||||
} else { // check non-string types
|
||||
switch ($column->getType()) {
|
||||
case 'integer':
|
||||
case 'bigint':
|
||||
if (!is_int($value)) {
|
||||
return 'invalid integer';
|
||||
}
|
||||
break;
|
||||
case 'float':
|
||||
case 'double':
|
||||
if (!is_float($value) && !is_int($value)) {
|
||||
return 'invalid float';
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
if (!is_bool($value) && ($value !== 0) && ($value !== 1)) {
|
||||
return 'invalid boolean';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return 'invalid ' . $column->getType();
|
||||
}
|
||||
}
|
||||
// extra checks
|
||||
switch ($column->getType()) {
|
||||
case 'integer': // 4 byte signed
|
||||
$value = filter_var($value, FILTER_VALIDATE_INT);
|
||||
if ($value > 2147483647 || $value < -2147483648) {
|
||||
return 'invalid integer';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (true);
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
|
||||
{
|
||||
$operation = RequestUtils::getOperation($request);
|
||||
if (in_array($operation, ['create', 'update', 'increment'])) {
|
||||
$tableName = RequestUtils::getPathSegment($request, 2);
|
||||
if ($this->reflection->hasTable($tableName)) {
|
||||
$record = $request->getParsedBody();
|
||||
if ($record !== null) {
|
||||
$handler = $this->getProperty('handler', '');
|
||||
if ($handler !== '') {
|
||||
$table = $this->reflection->getTable($tableName);
|
||||
if (is_array($record)) {
|
||||
foreach ($record as $r) {
|
||||
$response = $this->callHandler($handler, $r, $operation, $table);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$response = $this->callHandler($handler, $record, $operation, $table);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $next->handle($request);
|
||||
}
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
|
||||
{
|
||||
$operation = RequestUtils::getOperation($request);
|
||||
if (in_array($operation, ['create', 'update', 'increment'])) {
|
||||
$tableName = RequestUtils::getPathSegment($request, 2);
|
||||
if ($this->reflection->hasTable($tableName)) {
|
||||
$record = $request->getParsedBody();
|
||||
if ($record !== null) {
|
||||
$handler = $this->getProperty('handler', '');
|
||||
if ($handler !== '') {
|
||||
$table = $this->reflection->getTable($tableName);
|
||||
if (is_array($record)) {
|
||||
foreach ($record as $r) {
|
||||
$response = $this->callHandler($handler, $r, $operation, $table);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$response = $this->callHandler($handler, $record, $operation, $table);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $next->handle($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tqdev\PhpCrudApi\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Tqdev\PhpCrudApi\Column\ReflectionService;
|
||||
use Tqdev\PhpCrudApi\Controller\Responder;
|
||||
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
|
||||
use Tqdev\PhpCrudApi\Middleware\Router\Router;
|
||||
use Tqdev\PhpCrudApi\RequestUtils;
|
||||
use Tqdev\PhpCrudApi\ResponseFactory;
|
||||
|
||||
class XmlMiddleware extends Middleware
|
||||
{
|
||||
private $reflection;
|
||||
|
||||
public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
|
||||
{
|
||||
parent::__construct($router, $responder, $properties);
|
||||
$this->reflection = $reflection;
|
||||
}
|
||||
|
||||
private function json2xml($json, $types = 'null,boolean,number,string,object,array')
|
||||
{
|
||||
$a = json_decode($json);
|
||||
$d = new \DOMDocument();
|
||||
$c = $d->createElement("root");
|
||||
$d->appendChild($c);
|
||||
$t = function ($v) {
|
||||
$type = gettype($v);
|
||||
switch ($type) {
|
||||
case 'integer':
|
||||
return 'number';
|
||||
case 'double':
|
||||
return 'number';
|
||||
default:
|
||||
return strtolower($type);
|
||||
}
|
||||
};
|
||||
$ts = explode(',', $types);
|
||||
$f = function ($f, $c, $a, $s = false) use ($t, $d, $ts) {
|
||||
if (in_array($t($a), $ts)) {
|
||||
$c->setAttribute('type', $t($a));
|
||||
}
|
||||
if ($t($a) != 'array' && $t($a) != 'object') {
|
||||
if ($t($a) == 'boolean') {
|
||||
$c->appendChild($d->createTextNode($a ? 'true' : 'false'));
|
||||
} else {
|
||||
$c->appendChild($d->createTextNode($a));
|
||||
}
|
||||
} else {
|
||||
foreach ($a as $k => $v) {
|
||||
if ($k == '__type' && $t($a) == 'object') {
|
||||
$c->setAttribute('__type', $v);
|
||||
} else {
|
||||
if ($t($v) == 'object') {
|
||||
$ch = $c->appendChild($d->createElementNS(null, $s ? 'item' : $k));
|
||||
$f($f, $ch, $v);
|
||||
} else if ($t($v) == 'array') {
|
||||
$ch = $c->appendChild($d->createElementNS(null, $s ? 'item' : $k));
|
||||
$f($f, $ch, $v, true);
|
||||
} else {
|
||||
$va = $d->createElementNS(null, $s ? 'item' : $k);
|
||||
if ($t($v) == 'boolean') {
|
||||
$va->appendChild($d->createTextNode($v ? 'true' : 'false'));
|
||||
} else {
|
||||
$va->appendChild($d->createTextNode($v));
|
||||
}
|
||||
$ch = $c->appendChild($va);
|
||||
if (in_array($t($v), $ts)) {
|
||||
$ch->setAttribute('type', $t($v));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
$f($f, $c, $a, $t($a) == 'array');
|
||||
return $d->saveXML($d->documentElement);
|
||||
}
|
||||
|
||||
private function xml2json($xml)
|
||||
{
|
||||
$a = @dom_import_simplexml(simplexml_load_string($xml));
|
||||
if (!$a) {
|
||||
return null;
|
||||
}
|
||||
$t = function ($v) {
|
||||
$t = $v->getAttribute('type');
|
||||
$txt = $v->firstChild->nodeType == XML_TEXT_NODE;
|
||||
return $t ?: ($txt ? 'string' : 'object');
|
||||
};
|
||||
$f = function ($f, $a) use ($t) {
|
||||
$c = null;
|
||||
if ($t($a) == 'null') {
|
||||
$c = null;
|
||||
} else if ($t($a) == 'boolean') {
|
||||
$b = substr(strtolower($a->textContent), 0, 1);
|
||||
$c = in_array($b, array('1', 't'));
|
||||
} else if ($t($a) == 'number') {
|
||||
$c = $a->textContent + 0;
|
||||
} else if ($t($a) == 'string') {
|
||||
$c = $a->textContent;
|
||||
} else if ($t($a) == 'object') {
|
||||
$c = array();
|
||||
if ($a->getAttribute('__type')) {
|
||||
$c['__type'] = $a->getAttribute('__type');
|
||||
}
|
||||
for ($i = 0; $i < $a->childNodes->length; $i++) {
|
||||
$v = $a->childNodes[$i];
|
||||
$c[$v->nodeName] = $f($f, $v);
|
||||
}
|
||||
$c = (object) $c;
|
||||
} else if ($t($a) == 'array') {
|
||||
$c = array();
|
||||
for ($i = 0; $i < $a->childNodes->length; $i++) {
|
||||
$v = $a->childNodes[$i];
|
||||
$c[$i] = $f($f, $v);
|
||||
}
|
||||
}
|
||||
return $c;
|
||||
};
|
||||
$c = $f($f, $a);
|
||||
return json_encode($c);
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
|
||||
{
|
||||
parse_str($request->getUri()->getQuery(), $params);
|
||||
$isXml = isset($params['format']) && $params['format'] == 'xml';
|
||||
if ($isXml) {
|
||||
$body = $request->getBody()->getContents();
|
||||
if ($body) {
|
||||
$json = $this->xml2json($body);
|
||||
$request = $request->withParsedBody(json_decode($json));
|
||||
}
|
||||
}
|
||||
$response = $next->handle($request);
|
||||
if ($isXml) {
|
||||
$body = $response->getBody()->getContents();
|
||||
if ($body) {
|
||||
$types = implode(',', $this->getArrayProperty('types', 'null,array'));
|
||||
if ($types == '' || $types == 'all') {
|
||||
$xml = $this->json2xml($body);
|
||||
} else {
|
||||
$xml = $this->json2xml($body, $types);
|
||||
}
|
||||
$response = ResponseFactory::fromXml(ResponseFactory::OK, $xml);
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Tqdev\PhpCrudApi\OpenApi;
|
||||
|
||||
use Tqdev\PhpCrudApi\Column\ReflectionService;
|
||||
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
|
||||
use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
|
||||
use Tqdev\PhpCrudApi\OpenApi\OpenApiDefinition;
|
||||
|
||||
@@ -23,16 +22,16 @@ class OpenApiRecordsBuilder
|
||||
'integer' => ['type' => 'integer', 'format' => 'int32'],
|
||||
'bigint' => ['type' => 'integer', 'format' => 'int64'],
|
||||
'varchar' => ['type' => 'string'],
|
||||
'clob' => ['type' => 'string', 'format' => 'large-string'], //custom format
|
||||
'clob' => ['type' => 'string'],
|
||||
'varbinary' => ['type' => 'string', 'format' => 'byte'],
|
||||
'blob' => ['type' => 'string', 'format' => 'large-byte'], //custom format
|
||||
'decimal' => ['type' => 'string', 'format' => 'decimal'], //custom format
|
||||
'blob' => ['type' => 'string', 'format' => 'byte'],
|
||||
'decimal' => ['type' => 'string'],
|
||||
'float' => ['type' => 'number', 'format' => 'float'],
|
||||
'double' => ['type' => 'number', 'format' => 'double'],
|
||||
'date' => ['type' => 'string', 'format' => 'date'],
|
||||
'time' => ['type' => 'string', 'format' => 'time'], //custom format
|
||||
'time' => ['type' => 'string', 'format' => 'date-time'],
|
||||
'timestamp' => ['type' => 'string', 'format' => 'date-time'],
|
||||
'geometry' => ['type' => 'string', 'format' => 'geometry'], //custom format
|
||||
'geometry' => ['type' => 'string'],
|
||||
'boolean' => ['type' => 'boolean'],
|
||||
];
|
||||
|
||||
@@ -169,49 +168,6 @@ class OpenApiRecordsBuilder
|
||||
}
|
||||
}
|
||||
|
||||
private function getPattern(ReflectedColumn $column): string
|
||||
{
|
||||
switch ($column->getType()) {
|
||||
case 'integer':
|
||||
$n = strlen(pow(2, 31));
|
||||
return '^-?[0-9]{1,' . $n . '}$';
|
||||
case 'bigint':
|
||||
$n = strlen(pow(2, 63));
|
||||
return '^-?[0-9]{1,' . $n . '}$';
|
||||
case 'varchar':
|
||||
$l = $column->getLength();
|
||||
return '^.{0,' . $l . '}$';
|
||||
case 'clob':
|
||||
return '^.*$';
|
||||
case 'varbinary':
|
||||
$l = $column->getLength();
|
||||
$b = (int) 4 * ceil($l / 3);
|
||||
return '^[A-Za-z0-9+/]{0,' . $b . '}=*$';
|
||||
case 'blob':
|
||||
return '^[A-Za-z0-9+/]*=*$';
|
||||
case 'decimal':
|
||||
$p = $column->getPrecision();
|
||||
$s = $column->getScale();
|
||||
return '^-?[0-9]{1,' . ($p - $s) . '}(\.[0-9]{1,' . $s . '})?$';
|
||||
case 'float':
|
||||
return '^-?[0-9]+(\.[0-9]+)?([eE]-?[0-9]+)?$';
|
||||
case 'double':
|
||||
return '^-?[0-9]+(\.[0-9]+)?([eE]-?[0-9]+)?$';
|
||||
case 'date':
|
||||
return '^[0-9]{4}-[0-9]{2}-[0-9]{2}$';
|
||||
case 'time':
|
||||
return '^[0-9]{2}:[0-9]{2}:[0-9]{2}$';
|
||||
case 'timestamp':
|
||||
return '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$';
|
||||
return '';
|
||||
case 'geometry':
|
||||
return '^(POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON)\s*\(.*$';
|
||||
case 'boolean':
|
||||
return '^(true|false)$';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function setComponentSchema(string $tableName, array $references) /*: void*/
|
||||
{
|
||||
$table = $this->reflection->getTable($tableName);
|
||||
@@ -222,10 +178,7 @@ class OpenApiRecordsBuilder
|
||||
if (!$pkName && $operation != 'list') {
|
||||
continue;
|
||||
}
|
||||
if ($type == 'view' && !in_array($operation, array('read', 'list'))) {
|
||||
continue;
|
||||
}
|
||||
if ($type == 'view' && !$pkName && $operation == 'read') {
|
||||
if ($type != 'table' && $operation != 'list') {
|
||||
continue;
|
||||
}
|
||||
if ($operation == 'delete') {
|
||||
@@ -250,13 +203,8 @@ class OpenApiRecordsBuilder
|
||||
}
|
||||
$column = $table->getColumn($columnName);
|
||||
$properties = $this->types[$column->getType()];
|
||||
$properties['maxLength'] = $column->hasLength() ? $column->getLength() : 0;
|
||||
$properties['nullable'] = $column->getNullable();
|
||||
$properties['pattern'] = $this->getPattern($column);
|
||||
foreach ($properties as $key => $value) {
|
||||
if ($value) {
|
||||
$this->openapi->set("$prefix|properties|$columnName|$key", $value);
|
||||
}
|
||||
$this->openapi->set("$prefix|properties|$columnName|$key", $value);
|
||||
}
|
||||
if ($column->getPk()) {
|
||||
$this->openapi->set("$prefix|properties|$columnName|x-primary-key", true);
|
||||
|
||||
@@ -31,8 +31,6 @@ class ErrorCode
|
||||
const BAD_OR_MISSING_XSRF_TOKEN = 1017;
|
||||
const ONLY_AJAX_REQUESTS_ALLOWED = 1018;
|
||||
const PAGINATION_FORBIDDEN = 1019;
|
||||
const USER_ALREADY_EXIST = 1020;
|
||||
const PASSWORD_TOO_SHORT = 1021;
|
||||
|
||||
private $values = [
|
||||
9999 => ["%s", ResponseFactory::INTERNAL_SERVER_ERROR],
|
||||
@@ -56,8 +54,6 @@ class ErrorCode
|
||||
1017 => ["Bad or missing XSRF token", ResponseFactory::FORBIDDEN],
|
||||
1018 => ["Only AJAX requests allowed for '%s'", ResponseFactory::FORBIDDEN],
|
||||
1019 => ["Pagination forbidden", ResponseFactory::FORBIDDEN],
|
||||
1020 => ["User '%s' already exists", ResponseFactory::CONFLICT],
|
||||
1021 => ["Password too short (<%d characters)", ResponseFactory::UNPROCESSABLE_ENTITY],
|
||||
];
|
||||
|
||||
public function __construct(int $code)
|
||||
|
||||
@@ -10,19 +10,27 @@ use Tqdev\PhpCrudApi\Record\Condition\OrCondition;
|
||||
|
||||
class FilterInfo
|
||||
{
|
||||
private function addConditionFromFilterPath(PathTree $conditions, array $path, ReflectedTable $table, array $params)
|
||||
{
|
||||
$key = 'filter' . implode('', $path);
|
||||
if (isset($params[$key])) {
|
||||
foreach ($params[$key] as $filter) {
|
||||
$condition = Condition::fromString($table, $filter);
|
||||
if (($condition instanceof NoCondition) == false) {
|
||||
$conditions->put($path, $condition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getConditionsAsPathTree(ReflectedTable $table, array $params): PathTree
|
||||
{
|
||||
$conditions = new PathTree();
|
||||
foreach ($params as $key => $filters) {
|
||||
if (substr($key, 0, 6) == 'filter') {
|
||||
preg_match_all('/\d+|\D+/', substr($key, 6), $matches);
|
||||
$path = $matches[0];
|
||||
foreach ($filters as $filter) {
|
||||
$condition = Condition::fromString($table, $filter);
|
||||
if (($condition instanceof NoCondition) == false) {
|
||||
$conditions->put($path, $condition);
|
||||
}
|
||||
}
|
||||
$this->addConditionFromFilterPath($conditions, [], $table, $params);
|
||||
for ($n = ord('0'); $n <= ord('9'); $n++) {
|
||||
$this->addConditionFromFilterPath($conditions, [chr($n)], $table, $params);
|
||||
for ($l = ord('a'); $l <= ord('f'); $l++) {
|
||||
$this->addConditionFromFilterPath($conditions, [chr($n), chr($l)], $table, $params);
|
||||
}
|
||||
}
|
||||
return $conditions;
|
||||
|
||||
@@ -62,9 +62,6 @@ class RelationJoiner
|
||||
foreach ($params['join'] as $tableNames) {
|
||||
$path = array();
|
||||
foreach (explode(',', $tableNames) as $tableName) {
|
||||
if (!$this->reflection->hasTable($tableName)) {
|
||||
continue;
|
||||
}
|
||||
$t = $this->reflection->getTable($tableName);
|
||||
if ($t != null) {
|
||||
$path[] = $t->getName();
|
||||
|
||||
@@ -23,8 +23,7 @@ class RequestUtils
|
||||
{
|
||||
$params = array();
|
||||
$query = $request->getUri()->getQuery();
|
||||
//$query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
|
||||
$query = str_replace('%5D%5B%5D=', '%5D=', str_replace('=', '%5B%5D=', $query));
|
||||
$query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
|
||||
parse_str($query, $params);
|
||||
return $params;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use Psr\Http\Message\ResponseInterface;
|
||||
class ResponseFactory
|
||||
{
|
||||
const OK = 200;
|
||||
const MOVED_PERMANENTLY = 301;
|
||||
const FOUND = 302;
|
||||
const UNAUTHORIZED = 401;
|
||||
const FORBIDDEN = 403;
|
||||
const NOT_FOUND = 404;
|
||||
@@ -18,14 +16,10 @@ class ResponseFactory
|
||||
const UNPROCESSABLE_ENTITY = 422;
|
||||
const INTERNAL_SERVER_ERROR = 500;
|
||||
|
||||
public static function fromXml(int $status, string $xml): ResponseInterface
|
||||
{
|
||||
return self::from($status, 'text/xml', $xml);
|
||||
}
|
||||
|
||||
public static function fromCsv(int $status, string $csv): ResponseInterface
|
||||
{
|
||||
return self::from($status, 'text/csv', $csv);
|
||||
$response = self::from($status, 'text/csv', $csv);
|
||||
return $response->withHeader('Content-Type', 'text/csv');
|
||||
}
|
||||
|
||||
public static function fromHtml(int $status, string $html): ResponseInterface
|
||||
@@ -39,14 +33,14 @@ class ResponseFactory
|
||||
return self::from($status, 'application/json', $content);
|
||||
}
|
||||
|
||||
public static function from(int $status, string $contentType, string $content): ResponseInterface
|
||||
private static function from(int $status, string $contentType, string $content): ResponseInterface
|
||||
{
|
||||
$psr17Factory = new Psr17Factory();
|
||||
$response = $psr17Factory->createResponse($status);
|
||||
$stream = $psr17Factory->createStream($content);
|
||||
$stream->rewind();
|
||||
$response = $response->withBody($stream);
|
||||
$response = $response->withHeader('Content-Type', $contentType . '; charset=utf-8');
|
||||
$response = $response->withHeader('Content-Type', $contentType);
|
||||
$response = $response->withHeader('Content-Length', strlen($content));
|
||||
return $response;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,9 @@ use Tqdev\PhpCrudApi\ResponseUtils;
|
||||
require '../vendor/autoload.php';
|
||||
|
||||
$config = new Config([
|
||||
// 'driver' => 'mysql',
|
||||
// 'address' => 'localhost',
|
||||
// 'port' => '3306',
|
||||
'username' => 'php-crud-api',
|
||||
'password' => 'php-crud-api',
|
||||
'database' => 'php-crud-api',
|
||||
// 'debug' => false
|
||||
'database' => 'php-crud-api'
|
||||
]);
|
||||
$request = RequestFactory::fromGlobals();
|
||||
$api = new Api($config);
|
||||
|
||||
@@ -11,8 +11,7 @@ require 'vendor/autoload.php';
|
||||
function runDir(Config $config, string $dir, array $matches, string $category): array
|
||||
{
|
||||
$success = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
$total = 0;
|
||||
$entries = scandir($dir);
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry === '.' || $entry === '..') {
|
||||
@@ -28,77 +27,54 @@ function runDir(Config $config, string $dir, array $matches, string $category):
|
||||
if (substr($entry, -4) != '.log') {
|
||||
continue;
|
||||
}
|
||||
$statistics = runTest($config, $file, $category);
|
||||
$success += $statistics['success'];
|
||||
$skipped += $statistics['skipped'];
|
||||
$failed += $statistics['failed'];
|
||||
$success += runTest($config, $file, $category);
|
||||
$total += 1;
|
||||
} elseif (is_dir($file)) {
|
||||
$statistics = runDir($config, $file, array_slice($matches, 1), "$category/$entry");
|
||||
$total += $statistics['total'];
|
||||
$success += $statistics['success'];
|
||||
$skipped += $statistics['skipped'];
|
||||
$failed += $statistics['failed'];
|
||||
}
|
||||
}
|
||||
return compact('success', 'skipped', 'failed');
|
||||
$failed = $total - $success;
|
||||
return compact('total', 'success', 'failed');
|
||||
}
|
||||
|
||||
function runTest(Config $config, string $file, string $category): array
|
||||
function runTest(Config $config, string $file, string $category): int
|
||||
{
|
||||
$success = 1;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
$title = ucwords(str_replace('_', ' ', $category)) . '/';
|
||||
$title .= ucwords(str_replace('_', ' ', substr(basename($file), 0, -4)));
|
||||
$line1 = "=====[$title]=====";
|
||||
$len = strlen($line1);
|
||||
$line2 = str_repeat("=", $len);
|
||||
$parts = preg_split('/^[=]+([\r\n]+|$)/m', file_get_contents($file));
|
||||
$headers = explode("\n", $parts[0]);
|
||||
$driver = $config->getDriver();
|
||||
foreach ($headers as $header) {
|
||||
if (!strpos($header, ':')) {
|
||||
continue;
|
||||
$dirty = false;
|
||||
$success = 1;
|
||||
for ($i = 0; $i < count($parts); $i += 2) {
|
||||
$recording = false;
|
||||
if (empty($parts[$i + 1])) {
|
||||
if (substr($parts[$i], -1) != "\n") {
|
||||
$parts[$i] .= "\n";
|
||||
}
|
||||
$parts[$i + 1] = '';
|
||||
$recording = true;
|
||||
$dirty = true;
|
||||
}
|
||||
list($key, $value) = explode(':', strtolower($header));
|
||||
if ($key == "skip-for-$driver") {
|
||||
$skipped = 1;
|
||||
$success = 0;
|
||||
}
|
||||
if ($key == "skip-always") {
|
||||
$skipped = 1;
|
||||
$in = $parts[$i];
|
||||
$exp = $parts[$i + 1];
|
||||
$api = new Api($config);
|
||||
$_SERVER['REMOTE_ADDR'] = 'TEST_IP';
|
||||
$out = ResponseUtils::toString($api->handle(RequestFactory::fromString($in)));
|
||||
if ($recording) {
|
||||
$parts[$i + 1] = $out;
|
||||
} else if ($out != $exp) {
|
||||
echo "$line1\n$exp\n$line2\n$out\n$line2\n";
|
||||
$success = 0;
|
||||
}
|
||||
}
|
||||
if (!$skipped) {
|
||||
$dirty = false;
|
||||
for ($i = 1; $i < count($parts); $i += 2) {
|
||||
$recording = false;
|
||||
if (empty($parts[$i + 1])) {
|
||||
if (substr($parts[$i], -1) != "\n") {
|
||||
$parts[$i] .= "\n";
|
||||
}
|
||||
$parts[$i + 1] = '';
|
||||
$recording = true;
|
||||
$dirty = true;
|
||||
}
|
||||
$in = $parts[$i];
|
||||
$exp = $parts[$i + 1];
|
||||
$api = new Api($config);
|
||||
$_SERVER['REMOTE_ADDR'] = 'TEST_IP';
|
||||
$out = ResponseUtils::toString($api->handle(RequestFactory::fromString($in)));
|
||||
if ($recording) {
|
||||
$parts[$i + 1] = $out;
|
||||
} else if ($out != $exp) {
|
||||
echo "$line1\n$exp\n$line2\n$out\n$line2\n";
|
||||
$failed = 1;
|
||||
$success = 0;
|
||||
}
|
||||
}
|
||||
if ($dirty) {
|
||||
file_put_contents($file, implode("===\n", $parts));
|
||||
}
|
||||
if ($dirty) {
|
||||
file_put_contents($file, implode("===\n", $parts));
|
||||
}
|
||||
return compact('success', 'skipped', 'failed');
|
||||
return $success;
|
||||
}
|
||||
|
||||
function getDatabase(Config $config)
|
||||
@@ -188,15 +164,13 @@ function run(array $drivers, string $dir, array $matches)
|
||||
$config = new Config($settings);
|
||||
loadFixture($dir, $config);
|
||||
$start = microtime(true);
|
||||
$statistics = runDir($config, "$dir/functional", array_slice($matches, 1), '');
|
||||
$stats = runDir($config, "$dir/functional", array_slice($matches, 1), '');
|
||||
$end = microtime(true);
|
||||
$time = ($end - $start) * 1000;
|
||||
$success = $statistics['success'];
|
||||
$skipped = $statistics['skipped'];
|
||||
$failed = $statistics['failed'];
|
||||
$total = $success + $skipped + $failed;
|
||||
echo sprintf("%s: %d tests ran in %d ms, %d skipped, %d failed\n", $driver, $total, $time, $skipped, $failed);
|
||||
$total = $stats['total'];
|
||||
$failed = $stats['failed'];
|
||||
echo sprintf("%s: %d tests ran in %d ms, %d failed\n", $driver, $total, $time, $failed);
|
||||
}
|
||||
}
|
||||
|
||||
run(['mysql', 'pgsql', 'sqlsrv', 'sqlite'], __DIR__ . '/tests', array_slice($argv, 1));
|
||||
run(['mysql', 'pgsql', 'sqlsrv'], __DIR__ . '/tests', array_slice($argv, 1));
|
||||
|
||||
@@ -4,11 +4,9 @@ $settings = [
|
||||
'username' => 'incorrect_username',
|
||||
'password' => 'incorrect_password',
|
||||
'controllers' => 'records,columns,cache,openapi,geojson',
|
||||
'middlewares' => 'sslRedirect,xml,cors,reconnect,dbAuth,jwtAuth,basicAuth,authorization,sanitation,validation,ipAddress,multiTenancy,pageLimits,joinLimits,customization',
|
||||
'middlewares' => 'cors,reconnect,dbAuth,jwtAuth,basicAuth,authorization,validation,ipAddress,sanitation,multiTenancy,pageLimits,joinLimits,customization',
|
||||
'dbAuth.mode' => 'optional',
|
||||
'dbAuth.returnedColumns' => 'id,username,password',
|
||||
'dbAuth.registerUser' => '1',
|
||||
'dbAuth.passwordLength' => '4',
|
||||
'jwtAuth.mode' => 'optional',
|
||||
'jwtAuth.time' => '1538207605',
|
||||
'jwtAuth.secrets' => 'axpIrCGNGqxzx2R9dtXLIPUSqPo778uhb8CA0F4Hx',
|
||||
@@ -37,7 +35,6 @@ $settings = [
|
||||
'sanitation.handler' => function ($operation, $tableName, $column, $value) {
|
||||
return is_string($value) ? strip_tags($value) : $value;
|
||||
},
|
||||
'sanitation.tables' => 'forgiving',
|
||||
'validation.handler' => function ($operation, $tableName, $column, $value, $context) {
|
||||
return ($column['name'] == 'post_id' && !is_numeric($value)) ? 'must be numeric' : true;
|
||||
},
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
<?php
|
||||
$settings['driver'] = 'sqlite';
|
||||
BIN
seatmap-webapi/tests/fixtures/blog.sqlite
vendored
BIN
seatmap-webapi/tests/fixtures/blog.sqlite
vendored
Binary file not shown.
12
seatmap-webapi/tests/fixtures/blog_mysql.sql
vendored
12
seatmap-webapi/tests/fixtures/blog_mysql.sql
vendored
@@ -130,7 +130,7 @@ INSERT INTO `events` (`name`, `datetime`, `visitors`) VALUES
|
||||
('Launch', '2016-01-01 13:01:01', 0);
|
||||
|
||||
DROP VIEW IF EXISTS `tag_usage`;
|
||||
CREATE VIEW `tag_usage` AS select `tags`.`id` as `id`, `name`, count(`name`) AS `count` from `tags`, `post_tags` where `tags`.`id` = `post_tags`.`tag_id` group by `tags`.`id`, `name` order by `count` desc, `name`;
|
||||
CREATE VIEW `tag_usage` AS select `name`, count(`name`) AS `count` from `tags`, `post_tags` where `tags`.`id` = `post_tags`.`tag_id` group by `name` order by `count` desc, `name`;
|
||||
|
||||
DROP TABLE IF EXISTS `products`;
|
||||
CREATE TABLE `products` (
|
||||
@@ -167,16 +167,14 @@ CREATE TABLE `kunsthåndværk` (
|
||||
`Umlauts ä_ö_ü-COUNT` int(11) NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`invisible` varchar(36),
|
||||
`invisible_id` varchar(36),
|
||||
PRIMARY KEY (`id`),
|
||||
CONSTRAINT `kunsthåndværk_Umlauts ä_ö_ü-COUNT_fkey` UNIQUE (`Umlauts ä_ö_ü-COUNT`),
|
||||
CONSTRAINT `kunsthåndværk_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
|
||||
CONSTRAINT `kunsthåndværk_invisible_id_fkey` FOREIGN KEY (`invisible_id`) REFERENCES `invisibles` (`id`)
|
||||
CONSTRAINT `kunsthåndværk_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
INSERT INTO `kunsthåndværk` (`id`, `Umlauts ä_ö_ü-COUNT`, `user_id`, `invisible`, `invisible_id`) VALUES
|
||||
('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, 1, NULL, 'e42c77c6-06a4-4502-816c-d112c7142e6d'),
|
||||
('e31ecfe6-591f-4660-9fbd-1a232083037f', 2, 2, NULL, 'e42c77c6-06a4-4502-816c-d112c7142e6d');
|
||||
INSERT INTO `kunsthåndværk` (`id`, `Umlauts ä_ö_ü-COUNT`, `user_id`, `invisible`) VALUES
|
||||
('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, 1, NULL),
|
||||
('e31ecfe6-591f-4660-9fbd-1a232083037f', 2, 2, NULL);
|
||||
|
||||
DROP TABLE IF EXISTS `invisibles`;
|
||||
CREATE TABLE `invisibles` (
|
||||
|
||||
26
seatmap-webapi/tests/fixtures/blog_pgsql.sql
vendored
26
seatmap-webapi/tests/fixtures/blog_pgsql.sql
vendored
@@ -127,7 +127,7 @@ CREATE TABLE events (
|
||||
-- Name: tag_usage; Type: VIEW; Schema: public; Owner: postgres; Tablespace:
|
||||
--
|
||||
|
||||
CREATE VIEW "tag_usage" AS select "tags"."id" as "id", "name", count("name") AS "count" from "tags", "post_tags" where "tags"."id" = "post_tags"."tag_id" group by "tags"."id", "name" order by "count" desc, "name";
|
||||
CREATE VIEW "tag_usage" AS select "name", count("name") AS "count" from "tags", "post_tags" where "tags"."id" = "post_tags"."tag_id" group by "name" order by "count" desc, "name";
|
||||
|
||||
--
|
||||
-- Name: products; Type: TABLE; Schema: public; Owner: postgres; Tablespace:
|
||||
@@ -162,8 +162,7 @@ CREATE TABLE "kunsthåndværk" (
|
||||
id character varying(36) NOT NULL,
|
||||
"Umlauts ä_ö_ü-COUNT" integer NOT NULL,
|
||||
user_id integer NOT NULL,
|
||||
invisible character varying(36),
|
||||
invisible_id character varying(36)
|
||||
invisible character varying(36)
|
||||
);
|
||||
|
||||
--
|
||||
@@ -277,9 +276,9 @@ INSERT INTO "barcodes" ("product_id", "hex", "bin", "ip_address") VALUES
|
||||
-- Data for Name: kunsthåndværk; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
INSERT INTO "kunsthåndværk" ("id", "Umlauts ä_ö_ü-COUNT", "user_id", "invisible", "invisible_id") VALUES
|
||||
('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, 1, NULL, 'e42c77c6-06a4-4502-816c-d112c7142e6d'),
|
||||
('e31ecfe6-591f-4660-9fbd-1a232083037f', 2, 2, NULL, 'e42c77c6-06a4-4502-816c-d112c7142e6d');
|
||||
INSERT INTO "kunsthåndværk" ("id", "Umlauts ä_ö_ü-COUNT", "user_id", "invisible") VALUES
|
||||
('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, 1, NULL),
|
||||
('e31ecfe6-591f-4660-9fbd-1a232083037f', 2, 2, NULL);
|
||||
|
||||
--
|
||||
-- Data for Name: invisibles; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||
@@ -457,13 +456,6 @@ CREATE INDEX "kunsthåndværk_Umlauts ä_ö_ü-COUNT_idx" ON "kunsthåndværk" U
|
||||
CREATE INDEX "kunsthåndværk_user_id_idx" ON "kunsthåndværk" USING btree (user_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: kunsthåndværk_invisible_id_idx; Type: INDEX; Schema: public; Owner: postgres; Tablespace:
|
||||
--
|
||||
|
||||
CREATE INDEX "kunsthåndværk_invisible_id_idx" ON "kunsthåndværk" USING btree (invisible_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: comments_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
@@ -527,7 +519,6 @@ ALTER TABLE ONLY barcodes
|
||||
ALTER TABLE ONLY "kunsthåndværk"
|
||||
ADD CONSTRAINT "kunsthåndværk_Umlauts ä_ö_ü-COUNT_uc" UNIQUE ("Umlauts ä_ö_ü-COUNT");
|
||||
|
||||
|
||||
--
|
||||
-- Name: kunsthåndværk_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
@@ -536,13 +527,6 @@ ALTER TABLE ONLY "kunsthåndværk"
|
||||
ADD CONSTRAINT "kunsthåndværk_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: kunsthåndværk_invisible_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY "kunsthåndværk"
|
||||
ADD CONSTRAINT "kunsthåndværk_invisible_id_fkey" FOREIGN KEY (invisible_id) REFERENCES invisibles(id);
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
||||
175
seatmap-webapi/tests/fixtures/blog_sqlite.sql
vendored
175
seatmap-webapi/tests/fixtures/blog_sqlite.sql
vendored
@@ -1,175 +0,0 @@
|
||||
-- Adminer 4.2.4 SQLite 3 dump
|
||||
|
||||
PRAGMA foreign_keys = off;
|
||||
|
||||
DROP TABLE IF EXISTS "categories";
|
||||
CREATE TABLE "categories" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"icon" blob NULL
|
||||
);
|
||||
|
||||
INSERT INTO "categories" ("id", "name", "icon") VALUES (1, 'announcement', NULL);
|
||||
INSERT INTO "categories" ("id", "name", "icon") VALUES (2, 'article', NULL);
|
||||
INSERT INTO "categories" ("id", "name", "icon") VALUES (3, 'comment', NULL);
|
||||
|
||||
DROP TABLE IF EXISTS "comments";
|
||||
CREATE TABLE "comments" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"post_id" integer NOT NULL,
|
||||
"message" VARCHAR(255) NOT NULL,
|
||||
"category_id" integer NOT NULL,
|
||||
FOREIGN KEY ("post_id") REFERENCES "posts" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
FOREIGN KEY ("category_id") REFERENCES "categories" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX "comments_post_id" ON "comments" ("post_id");
|
||||
CREATE INDEX "comments_category_id" ON "comments" ("category_id");
|
||||
|
||||
INSERT INTO "comments" ("id", "post_id", "message", "category_id") VALUES (1, 1, 'great', 3);
|
||||
INSERT INTO "comments" ("id", "post_id", "message", "category_id") VALUES (2, 1, 'fantastic', 3);
|
||||
INSERT INTO "comments" ("id", "post_id", "message", "category_id") VALUES (3, 2, 'thank you', 3);
|
||||
INSERT INTO "comments" ("id", "post_id", "message", "category_id") VALUES (4, 2, 'awesome', 3);
|
||||
|
||||
DROP TABLE IF EXISTS "posts";
|
||||
CREATE TABLE "posts" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" integer NOT NULL,
|
||||
"category_id" integer NOT NULL,
|
||||
"content" varchar(255) NOT NULL,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
FOREIGN KEY ("category_id") REFERENCES "categories" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX "posts_user_id" ON "posts" ("user_id");
|
||||
|
||||
CREATE INDEX "posts_category_id" ON "posts" ("category_id");
|
||||
|
||||
INSERT INTO "posts" ("id", "user_id", "category_id", "content") VALUES (1, 1, 1, 'blog started');
|
||||
INSERT INTO "posts" ("id", "user_id", "category_id", "content") VALUES (2, 1, 2, 'It works!');
|
||||
|
||||
DROP TABLE IF EXISTS "post_tags";
|
||||
CREATE TABLE "post_tags" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"post_id" integer NOT NULL,
|
||||
"tag_id" integer NOT NULL,
|
||||
FOREIGN KEY ("tag_id") REFERENCES "tags" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
FOREIGN KEY ("post_id") REFERENCES "posts" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "post_tags_post_id_tag_id" ON "post_tags" ("post_id", "tag_id");
|
||||
|
||||
INSERT INTO "post_tags" ("id", "post_id", "tag_id") VALUES (1, 1, 1);
|
||||
INSERT INTO "post_tags" ("id", "post_id", "tag_id") VALUES (2, 1, 2);
|
||||
INSERT INTO "post_tags" ("id", "post_id", "tag_id") VALUES (3, 2, 1);
|
||||
INSERT INTO "post_tags" ("id", "post_id", "tag_id") VALUES (4, 2, 2);
|
||||
|
||||
DROP TABLE IF EXISTS "tags";
|
||||
CREATE TABLE "tags" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"is_important" boolean NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "tags" ("id", "name", "is_important") VALUES (1, 'funny', 0);
|
||||
INSERT INTO "tags" ("id", "name", "is_important") VALUES (2, 'important', 1);
|
||||
|
||||
DROP TABLE IF EXISTS "users";
|
||||
CREATE TABLE "users" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"username" varchar(255) NOT NULL,
|
||||
"password" varchar(255) NOT NULL,
|
||||
"location" text NULL
|
||||
);
|
||||
|
||||
INSERT INTO "users" ("id", "username", "password", "location") VALUES (1, 'user1', 'pass1', NULL);
|
||||
INSERT INTO "users" ("id", "username", "password", "location") VALUES (2, 'user2', '$2y$10$cg7/nswxVZ0cmVIsMB/pVOh1OfcHScBJGq7Xu4KF9dFEQgRZ8HWe.', NULL);
|
||||
|
||||
DROP TABLE IF EXISTS "countries";
|
||||
CREATE TABLE "countries" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"shape" text NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "countries" ("id", "name", "shape") VALUES (1, 'Left', 'POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))');
|
||||
INSERT INTO "countries" ("id", "name", "shape") VALUES (2, 'Right', 'POLYGON ((70 10, 80 40, 60 40, 50 20, 70 10))');
|
||||
INSERT INTO "countries" ("id", "name", "shape") VALUES (3, 'Point', 'POINT (30 10)');
|
||||
INSERT INTO "countries" ("id", "name", "shape") VALUES (4, 'Line', 'LINESTRING (30 10, 10 30, 40 40)');
|
||||
INSERT INTO "countries" ("id", "name", "shape") VALUES (5, 'Poly1', 'POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))');
|
||||
INSERT INTO "countries" ("id", "name", "shape") VALUES (6, 'Poly2', 'POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10),(20 30, 35 35, 30 20, 20 30))');
|
||||
INSERT INTO "countries" ("id", "name", "shape") VALUES (7, 'Mpoint', 'MULTIPOINT (10 40, 40 30, 20 20, 30 10)');
|
||||
INSERT INTO "countries" ("id", "name", "shape") VALUES (8, 'Mline', 'MULTILINESTRING ((10 10, 20 20, 10 40),(40 40, 30 30, 40 20, 30 10))');
|
||||
INSERT INTO "countries" ("id", "name", "shape") VALUES (9, 'Mpoly1', 'MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)),((15 5, 40 10, 10 20, 5 10, 15 5)))');
|
||||
INSERT INTO "countries" ("id", "name", "shape") VALUES (10, 'Mpoly2', 'MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)),((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20)))');
|
||||
INSERT INTO "countries" ("id", "name", "shape") VALUES (11, 'Gcoll', 'GEOMETRYCOLLECTION(POINT(4 6),LINESTRING(4 6,7 10))');
|
||||
|
||||
DROP TABLE IF EXISTS "events";
|
||||
CREATE TABLE "events" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"datetime" datetime,
|
||||
"visitors" bigint
|
||||
);
|
||||
|
||||
INSERT INTO "events" ("id", "name", "datetime", "visitors") VALUES (1, 'Launch', '2016-01-01 13:01:01', 0);
|
||||
|
||||
DROP VIEW IF EXISTS "tag_usage";
|
||||
CREATE VIEW "tag_usage" AS select "tags"."id" as "id", "name", count("name") AS "count" from "tags", "post_tags" where "tags"."id" = "post_tags"."tag_id" group by "tags"."id", "name" order by "count" desc, "name";
|
||||
|
||||
DROP TABLE IF EXISTS "products";
|
||||
CREATE TABLE "products" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"price" decimal(10,2) NOT NULL,
|
||||
"properties" clob NOT NULL,
|
||||
"created_at" datetime NOT NULL,
|
||||
"deleted_at" datetime NULL
|
||||
);
|
||||
|
||||
INSERT INTO "products" ("id", "name", "price", "properties", "created_at") VALUES (1, 'Calculator', '23.01', '{"depth":false,"model":"TRX-120","width":100,"height":null}', '1970-01-01 01:01:01');
|
||||
|
||||
DROP TABLE IF EXISTS "barcodes2";
|
||||
DROP TABLE IF EXISTS "barcodes";
|
||||
CREATE TABLE "barcodes" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"product_id" integer NOT NULL,
|
||||
"hex" varchar(255) NOT NULL,
|
||||
"bin" blob NOT NULL,
|
||||
"ip_address" varchar(15),
|
||||
FOREIGN KEY ("product_id") REFERENCES "products" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
INSERT INTO "barcodes" ("id", "product_id", "hex", "bin", "ip_address") VALUES (1, 1, '00ff01', 'AP8B', '127.0.0.1');
|
||||
|
||||
DROP TABLE IF EXISTS "kunsthåndværk";
|
||||
CREATE TABLE "kunsthåndværk" (
|
||||
"id" varchar(36) NOT NULL PRIMARY KEY,
|
||||
"Umlauts ä_ö_ü-COUNT" integer NOT NULL UNIQUE,
|
||||
"user_id" integer NOT NULL,
|
||||
"invisible" varchar(36),
|
||||
"invisible_id" varchar(36),
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
FOREIGN KEY ("invisible_id") REFERENCES "invisibles" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
);
|
||||
|
||||
INSERT INTO "kunsthåndværk" ("id", "Umlauts ä_ö_ü-COUNT", "user_id", "invisible", "invisible_id") VALUES ('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, 1, NULL, 'e42c77c6-06a4-4502-816c-d112c7142e6d');
|
||||
INSERT INTO "kunsthåndværk" ("id", "Umlauts ä_ö_ü-COUNT", "user_id", "invisible", "invisible_id") VALUES ('e31ecfe6-591f-4660-9fbd-1a232083037f', 2, 2, NULL, 'e42c77c6-06a4-4502-816c-d112c7142e6d');
|
||||
|
||||
DROP TABLE IF EXISTS "invisibles";
|
||||
CREATE TABLE "invisibles" (
|
||||
"id" varchar(36) NOT NULL PRIMARY KEY
|
||||
);
|
||||
|
||||
INSERT INTO "invisibles" ("id") VALUES ('e42c77c6-06a4-4502-816c-d112c7142e6d');
|
||||
|
||||
DROP TABLE IF EXISTS "nopk";
|
||||
CREATE TABLE "nopk" (
|
||||
"id" varchar(36) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "nopk" ("id") VALUES ('e42c77c6-06a4-4502-816c-d112c7142e6d');
|
||||
|
||||
PRAGMA foreign_keys = on;
|
||||
|
||||
--
|
||||
13
seatmap-webapi/tests/fixtures/blog_sqlsrv.sql
vendored
13
seatmap-webapi/tests/fixtures/blog_sqlsrv.sql
vendored
@@ -246,7 +246,7 @@ GO
|
||||
|
||||
CREATE VIEW [tag_usage]
|
||||
AS
|
||||
SELECT top 100 PERCENT tags.id as id, name, COUNT_BIG(name) AS [count] FROM tags, post_tags WHERE tags.id = post_tags.tag_id GROUP BY tags.id, name ORDER BY [count] DESC, name
|
||||
SELECT top 100 PERCENT name, COUNT_BIG(name) AS [count] FROM tags, post_tags WHERE tags.id = post_tags.tag_id GROUP BY name ORDER BY [count] DESC, name
|
||||
GO
|
||||
|
||||
DROP SEQUENCE IF EXISTS [products_id_seq]
|
||||
@@ -285,7 +285,6 @@ CREATE TABLE [kunsthåndværk](
|
||||
[Umlauts ä_ö_ü-COUNT] [int] NOT NULL,
|
||||
[user_id] [int] NOT NULL,
|
||||
[invisible] [nvarchar](36),
|
||||
[invisible_id] [nvarchar](36),
|
||||
CONSTRAINT [kunsthåndværk_pkey] PRIMARY KEY CLUSTERED([id] ASC)
|
||||
)
|
||||
GO
|
||||
@@ -373,9 +372,9 @@ GO
|
||||
INSERT [barcodes] ([product_id], [hex], [bin], [ip_address]) VALUES (1, N'00ff01', 0x00ff01, N'127.0.0.1')
|
||||
GO
|
||||
|
||||
INSERT [kunsthåndværk] ([id], [Umlauts ä_ö_ü-COUNT], [user_id], [invisible], [invisible_id]) VALUES ('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, 1, NULL, 'e42c77c6-06a4-4502-816c-d112c7142e6d')
|
||||
INSERT [kunsthåndværk] ([id], [Umlauts ä_ö_ü-COUNT], [user_id], [invisible]) VALUES ('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, 1, NULL)
|
||||
GO
|
||||
INSERT [kunsthåndværk] ([id], [Umlauts ä_ö_ü-COUNT], [user_id], [invisible], [invisible_id]) VALUES ('e31ecfe6-591f-4660-9fbd-1a232083037f', 2, 2, NULL, 'e42c77c6-06a4-4502-816c-d112c7142e6d')
|
||||
INSERT [kunsthåndværk] ([id], [Umlauts ä_ö_ü-COUNT], [user_id], [invisible]) VALUES ('e31ecfe6-591f-4660-9fbd-1a232083037f', 2, 2, NULL)
|
||||
GO
|
||||
|
||||
INSERT [invisibles] ([id]) VALUES ('e42c77c6-06a4-4502-816c-d112c7142e6d')
|
||||
@@ -434,9 +433,3 @@ REFERENCES [users] ([id])
|
||||
GO
|
||||
ALTER TABLE [kunsthåndværk] CHECK CONSTRAINT [kunsthåndværk_user_id_fkey]
|
||||
GO
|
||||
|
||||
ALTER TABLE [kunsthåndværk] WITH CHECK ADD CONSTRAINT [kunsthåndværk_invisible_id_fkey] FOREIGN KEY([invisible_id])
|
||||
REFERENCES [invisibles] ([id])
|
||||
GO
|
||||
ALTER TABLE [kunsthåndværk] CHECK CONSTRAINT [kunsthåndværk_invisible_id_fkey]
|
||||
GO
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 134
|
||||
|
||||
{"records":[{"id":1,"user_id":1,"category_id":1,"content":"blog started"},{"id":2,"user_id":1,"category_id":2,"content":"It works!"}]}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?include=id,content
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 78
|
||||
|
||||
{"records":[{"id":1,"content":"blog started"},{"id":2,"content":"It works!"}]}
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
===
|
||||
GET /records/posts/2
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 58
|
||||
|
||||
{"id":2,"user_id":1,"category_id":2,"content":"It works!"}
|
||||
===
|
||||
GET /records/posts/0
|
||||
===
|
||||
404
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Length: 46
|
||||
|
||||
{"code":1003,"message":"Record '0' not found"}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts/1,2
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 122
|
||||
|
||||
[{"id":1,"user_id":1,"category_id":1,"content":"blog started"},{"id":2,"user_id":1,"category_id":2,"content":"It works!"}]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts/2?include=id,content
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 30
|
||||
|
||||
{"id":2,"content":"It works!"}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
===
|
||||
POST /records/posts
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
|
||||
{"user_id":1,"category_id":1,"content":"test"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
3
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
===
|
||||
PUT /records/posts/3
|
||||
|
||||
{"user_id":1,"category_id":1,"content":"test (edited)"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
1
|
||||
@@ -12,7 +11,7 @@ Content-Length: 1
|
||||
GET /records/posts/3
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 62
|
||||
|
||||
{"id":3,"user_id":1,"category_id":1,"content":"test (edited)"}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
===
|
||||
PUT /records/posts/3?include=id,content
|
||||
|
||||
{"content":"test (edited 2)"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
1
|
||||
@@ -12,7 +11,7 @@ Content-Length: 1
|
||||
GET /records/posts/3
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 64
|
||||
|
||||
{"id":3,"user_id":1,"category_id":1,"content":"test (edited 2)"}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
===
|
||||
PUT /records/posts/3?include=id,content
|
||||
|
||||
{"user_id":2,"content":"test (edited 3)"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
1
|
||||
@@ -12,7 +11,7 @@ Content-Length: 1
|
||||
GET /records/posts/3
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 64
|
||||
|
||||
{"id":3,"user_id":1,"category_id":1,"content":"test (edited 3)"}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
===
|
||||
PUT /records/posts/2
|
||||
|
||||
{"content":"🤗 Grüßgott, Вiтаю, dobrý deň, hyvää päivää, გამარჯობა, Γεια σας, góðan dag, здравствуйте"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
1
|
||||
@@ -12,7 +11,7 @@ Content-Length: 1
|
||||
GET /records/posts/2
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 192
|
||||
|
||||
{"id":2,"user_id":1,"category_id":2,"content":"🤗 Grüßgott, Вiтаю, dobrý deň, hyvää päivää, გამარჯობა, Γεια σας, góðan dag, здравствуйте"}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
===
|
||||
PUT /records/posts/2
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
content=%F0%9F%A6%80%E2%82%AC%20Gr%C3%BC%C3%9Fgott%2C%20%D0%92i%D1%82%D0%B0%D1%8E%2C%20dobr%C3%BD%20de%C5%88%2C%20hyv%C3%A4%C3%A4%20p%C3%A4iv%C3%A4%C3%A4%2C%20%E1%83%92%E1%83%90%E1%83%9B%E1%83%90%E1%83%A0%E1%83%AF%E1%83%9D%E1%83%91%E1%83%90%2C%20%CE%93%CE%B5%CE%B9%CE%B1%20%CF%83%CE%B1%CF%82%2C%20g%C3%B3%C3%B0an%20dag%2C%20%D0%B7%D0%B4%D1%80%D0%B0%D0%B2%D1%81%D1%82%D0%B2%D1%83%D0%B9%D1%82%D0%B5
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
1
|
||||
@@ -13,7 +12,7 @@ Content-Length: 1
|
||||
GET /records/posts/2
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 195
|
||||
|
||||
{"id":2,"user_id":1,"category_id":2,"content":"🦀€ Grüßgott, Вiтаю, dobrý deň, hyvää päivää, გამარჯობა, Γεια σας, góðan dag, здравствуйте"}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
DELETE /records/posts/3
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
1
|
||||
@@ -10,7 +9,7 @@ Content-Length: 1
|
||||
GET /records/posts/3
|
||||
===
|
||||
404
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 46
|
||||
|
||||
{"code":1003,"message":"Record '3' not found"}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
===
|
||||
POST /records/posts
|
||||
|
||||
user_id=1&category_id=1&content=test
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
4
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
===
|
||||
PUT /records/posts/4
|
||||
|
||||
user_id=1&category_id=1&content=test+(edited)
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
1
|
||||
@@ -12,7 +11,7 @@ Content-Length: 1
|
||||
GET /records/posts/4
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 62
|
||||
|
||||
{"id":4,"user_id":1,"category_id":1,"content":"test (edited)"}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
DELETE /records/posts/4?include=id,content
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
1
|
||||
@@ -10,7 +9,7 @@ Content-Length: 1
|
||||
GET /records/posts/4
|
||||
===
|
||||
404
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 46
|
||||
|
||||
{"code":1003,"message":"Record '4' not found"}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
===
|
||||
POST /records/posts
|
||||
|
||||
{"user_id":1,"category_id":1,"content":"#1"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
5
|
||||
@@ -14,7 +13,7 @@ POST /records/posts
|
||||
{"user_id":1,"category_id":1,"content":"#2"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
6
|
||||
@@ -24,7 +23,7 @@ POST /records/posts
|
||||
{"user_id":1,"category_id":1,"content":"#3"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
7
|
||||
@@ -34,7 +33,7 @@ POST /records/posts
|
||||
{"user_id":1,"category_id":1,"content":"#4"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
8
|
||||
@@ -44,7 +43,7 @@ POST /records/posts
|
||||
{"user_id":1,"category_id":1,"content":"#5"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
9
|
||||
@@ -54,7 +53,7 @@ POST /records/posts
|
||||
{"user_id":1,"category_id":1,"content":"#6"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 2
|
||||
|
||||
10
|
||||
@@ -64,7 +63,7 @@ POST /records/posts
|
||||
{"user_id":1,"category_id":1,"content":"#7"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 2
|
||||
|
||||
11
|
||||
@@ -74,7 +73,7 @@ POST /records/posts
|
||||
{"user_id":1,"category_id":1,"content":"#8"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 2
|
||||
|
||||
12
|
||||
@@ -84,7 +83,7 @@ POST /records/posts
|
||||
{"user_id":1,"category_id":1,"content":"#9"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 2
|
||||
|
||||
13
|
||||
@@ -94,7 +93,7 @@ POST /records/posts
|
||||
{"user_id":1,"category_id":1,"content":"#10"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 2
|
||||
|
||||
14
|
||||
@@ -102,7 +101,7 @@ Content-Length: 2
|
||||
GET /records/posts?page=2,2&order=id
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 130
|
||||
|
||||
{"records":[{"id":5,"user_id":1,"category_id":1,"content":"#1"},{"id":6,"user_id":1,"category_id":1,"content":"#2"}],"results":12}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
===
|
||||
PUT /records/posts/2
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
|
||||
{"id":1}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
0
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
===
|
||||
POST /records/posts
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
|
||||
{"category_id":1,"content":"test"}
|
||||
===
|
||||
409
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 50
|
||||
|
||||
{"code":1010,"message":"Data integrity violation"}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?page=1,2&order=category_id,asc&order=id,desc
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 133
|
||||
|
||||
{"records":[{"id":14,"user_id":1,"category_id":1,"content":"#10"},{"id":13,"user_id":1,"category_id":1,"content":"#9"}],"results":12}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?page=2,2&order=id,desc
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 132
|
||||
|
||||
{"records":[{"id":12,"user_id":1,"category_id":1,"content":"#8"},{"id":11,"user_id":1,"category_id":1,"content":"#7"}],"results":12}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?order=id&size=1
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 75
|
||||
|
||||
{"records":[{"id":1,"user_id":1,"category_id":1,"content":"blog started"}]}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?order=id&page=1,0
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 27
|
||||
|
||||
{"records":[],"results":12}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?order=id&size=0
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 14
|
||||
|
||||
{"records":[]}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?page=3,5&order=id
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 133
|
||||
|
||||
{"records":[{"id":13,"user_id":1,"category_id":1,"content":"#9"},{"id":14,"user_id":1,"category_id":1,"content":"#10"}],"results":12}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?filter=id,eq,1
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 75
|
||||
|
||||
{"records":[{"id":1,"user_id":1,"category_id":1,"content":"blog started"}]}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?exclude=id&filter=id,eq,1
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 68
|
||||
|
||||
{"records":[{"user_id":1,"category_id":1,"content":"blog started"}]}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?join=users&filter=id,eq,1
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 136
|
||||
|
||||
{"records":[{"id":1,"user_id":{"id":1,"username":"user1","password":"pass1","location":null},"category_id":1,"content":"blog started"}]}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts/1?join=users
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 122
|
||||
|
||||
{"id":1,"user_id":{"id":1,"username":"user1","password":"pass1","location":null},"category_id":1,"content":"blog started"}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?join=comments&filter=id,eq,1
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 202
|
||||
|
||||
{"records":[{"id":1,"user_id":1,"category_id":1,"content":"blog started","comments":[{"id":1,"post_id":1,"message":"great","category_id":3},{"id":2,"post_id":1,"message":"fantastic","category_id":3}]}]}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?join=tags&filter=id,eq,1
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 177
|
||||
|
||||
{"records":[{"id":1,"user_id":1,"category_id":1,"content":"blog started","tags":[{"id":1,"name":"funny","is_important":false},{"id":2,"name":"important","is_important":true}]}]}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?join=categories&join=post_tags,tags&join=comments&filter=id,eq,1
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 410
|
||||
|
||||
{"records":[{"id":1,"user_id":1,"category_id":{"id":1,"name":"announcement","icon":null},"content":"blog started","post_tags":[{"id":1,"post_id":1,"tag_id":{"id":1,"name":"funny","is_important":false}},{"id":2,"post_id":1,"tag_id":{"id":2,"name":"important","is_important":true}}],"comments":[{"id":1,"post_id":1,"message":"great","category_id":3},{"id":2,"post_id":1,"message":"fantastic","category_id":3}]}]}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?join=categories&join=tags&join=comments&filter=id,eq,1
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 345
|
||||
|
||||
{"records":[{"id":1,"user_id":1,"category_id":{"id":1,"name":"announcement","icon":null},"content":"blog started","tags":[{"id":1,"name":"funny","is_important":false},{"id":2,"name":"important","is_important":true}],"comments":[{"id":1,"post_id":1,"message":"great","category_id":3},{"id":2,"post_id":1,"message":"fantastic","category_id":3}]}]}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
===
|
||||
GET /records/posts?include=tags.name&join=categories&join=post_tags,tags&join=comments&filter=id,eq,1
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 198
|
||||
|
||||
{"records":[{"id":1,"category_id":{"id":1},"post_tags":[{"post_id":1,"tag_id":{"id":1,"name":"funny"}},{"post_id":1,"tag_id":{"id":2,"name":"important"}}],"comments":[{"post_id":1},{"post_id":1}]}]}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
===
|
||||
GET /records/posts?join=categories&join=post_tags,tags&join=comments&exclude=comments.message,comments.category_id&filter=id,eq,1
|
||||
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 338
|
||||
|
||||
{"records":[{"id":1,"user_id":1,"category_id":{"id":1,"name":"announcement","icon":null},"content":"blog started","post_tags":[{"id":1,"post_id":1,"tag_id":{"id":1,"name":"funny","is_important":false}},{"id":2,"post_id":1,"tag_id":{"id":2,"name":"important","is_important":true}}],"comments":[{"id":1,"post_id":1},{"id":2,"post_id":1}]}]}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
===
|
||||
PUT /records/categories/2
|
||||
|
||||
{"icon":"4oKsIABhYmMACg1cYgA"}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
1
|
||||
@@ -12,7 +11,7 @@ Content-Length: 1
|
||||
GET /records/categories/2
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 55
|
||||
|
||||
{"id":2,"name":"article","icon":"4oKsIABhYmMACg1cYgA="}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
===
|
||||
PUT /records/categories/2
|
||||
|
||||
{"icon":""}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
1
|
||||
@@ -12,7 +11,7 @@ Content-Length: 1
|
||||
GET /records/categories/2
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 35
|
||||
|
||||
{"id":2,"name":"article","icon":""}
|
||||
@@ -22,7 +21,7 @@ PUT /records/categories/2
|
||||
{"icon":null}
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 1
|
||||
|
||||
1
|
||||
@@ -30,7 +29,7 @@ Content-Length: 1
|
||||
GET /records/categories/2
|
||||
===
|
||||
200
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Type: application/json
|
||||
Content-Length: 37
|
||||
|
||||
{"id":2,"name":"article","icon":null}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user