add the db scheme in the readme

This commit is contained in:
nas 2021-04-06 20:56:50 +02:00
commit 8aa68f8a0a
2 changed files with 219 additions and 60 deletions

128
README.md
View file

@ -1,4 +1,12 @@
# PHP-CRUD-API
# Todo API
## Database
![Database scheme in svg](docs/scheme_mld.svg "MCD")
Its relying on PHP-CRUD-API, with the original README here :
## PHP-CRUD-API
Single file PHP script that adds a REST API to a MySQL/MariaDB, PostgreSQL, SQL Server or SQLite database.
@ -25,7 +33,7 @@ There are also proof-of-concept ports of this script that only support basic RES
[Node.js](https://github.com/mevdschee/js-crud-api/blob/master/app.js) and
[Python](https://github.com/mevdschee/py-crud-api/blob/master/api.py).
## Requirements
### 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
@ -33,7 +41,7 @@ There are also proof-of-concept ports of this script that only support basic RES
- SQL Server 2012 or higher (2017 for Linux support)
- SQLite 3.16 or higher (spatial features NOT supported)
## Installation
### Installation
This is a single file application! Upload "`api.php`" somewhere and enjoy!
@ -58,7 +66,7 @@ In these integrations [Composer](https://getcomposer.org/) is used to load this
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
### Configuration
Edit the following lines in the bottom of the file "`api.php`":
@ -98,7 +106,7 @@ All configuration options are also available as environment variables. Write the
The environment variables take precedence over the PHP configuration.
## Limitations
### Limitations
These limitation and constrains apply:
@ -110,7 +118,7 @@ These limitation and constrains apply:
- SQLite cannot have bigint typed auto incrementing primary keys
- SQLite does not support altering table columns (structure)
## Features
### Features
The following features are supported:
@ -138,7 +146,7 @@ The following features are supported:
- Security enhancing middleware is included
- Standard compliant: PSR-4, PSR-7, PSR-12, PSR-15 and PSR-17
## Compilation
### Compilation
You can install all dependencies of this project using the following command:
@ -150,7 +158,7 @@ You can compile all files into a single "`api.php`" file using:
NB: The install script will patch the dependencies in the vendor directory for PHP 7.0 compatibility.
### Development
#### Development
You can access the non-compiled code at the URL:
@ -158,7 +166,7 @@ You can access the non-compiled code at the URL:
The non-compiled code resides in the "`src`" and "`vendor`" directories. The "`vendor`" directory contains the dependencies.
### Updating dependencies
#### Updating dependencies
You can update all dependencies of this project using the following command:
@ -168,13 +176,13 @@ This script will install and run [Composer](https://getcomposer.org/) to update
NB: The update script will patch the dependencies in the vendor directory for PHP 7.0 compatibility.
## TreeQL, a pragmatic GraphQL
### TreeQL, a pragmatic GraphQL
[TreeQL](https://treeql.org) allows you to create a "tree" of JSON objects based on your SQL database structure (relations) and your query.
It is loosely based on the REST standard and also inspired by json:api.
### CRUD + List
#### CRUD + List
The example posts table has only a a few fields:
@ -187,7 +195,7 @@ The example posts table has only a a few fields:
The CRUD + List operations below act on this table.
#### Create
##### Create
If you want to create a record the request can be written in URL format as:
@ -205,7 +213,7 @@ And it will return the value of the primary key of the newly created record:
2
#### Read
##### Read
To read a record from this table the request can be written in URL format as:
@ -222,7 +230,7 @@ Where "1" is the value of the primary key of the record that you want to read. I
On read operations you may apply joins.
#### Update
##### Update
To update a record in this table the request can be written in URL format as:
@ -238,7 +246,7 @@ This adjusts the title of the post. And the return value is the number of rows t
1
#### Delete
##### Delete
If you want to delete a record from this table the request can be written in URL format as:
@ -248,7 +256,7 @@ And it will return the number of deleted rows:
1
#### List
##### List
To list records from this table the request can be written in URL format as:
@ -269,7 +277,7 @@ It will return:
On list operations you may apply filters and joins.
### Filters
#### Filters
Filters provide search functionality, on list calls, using the "filter" parameter. You need to specify the column
name, a comma, the match type, another commma and the value you want to filter on. These are supported match types:
@ -309,7 +317,7 @@ Output:
In the next section we dive deeper into how you can apply multiple filters on a single list call.
### Multiple filters
#### Multiple filters
Filters can be a by applied by repeating the "filter" parameter in the URL. For example the following URL:
@ -325,7 +333,7 @@ by adding a letter (a-f) you can create almost any reasonably complex condition
NB: You can only filter on the requested table (not on it's included tables) and filters are only applied on list calls.
### Column selection
#### Column selection
By default all columns are selected. With the "include" parameter you can select specific columns.
You may use a dot to separate the table name from the column name. Multiple columns should be comma separated.
@ -347,7 +355,7 @@ Output:
NB: Columns that are used to include related entities are automatically added and cannot be left out of the output.
### Ordering
#### Ordering
With the "order" parameter you can sort. By default the sort is in ascending order, but by specifying "desc" this can be reversed:
@ -375,7 +383,7 @@ Output:
NB: You may sort on multiple fields by using multiple "order" parameters. You can not order on "joined" columns.
### Limit size
#### Limit size
The "size" parameter limits the number of returned records. This can be used for top N lists together with the "order" parameter (use descending order).
@ -398,7 +406,7 @@ Output:
NB: If you also want to know to the total number of records you may want to use the "page" parameter.
### Pagination
#### Pagination
The "page" parameter holds the requested page. The default page size is 20, but can be adjusted (e.g. to 50).
@ -427,7 +435,7 @@ Output:
NB: Since pages that are not ordered cannot be paginated, pages will be ordered by primary key.
### Joins
#### Joins
Let's say that you have a posts table that has comments (made by users) and the posts can have tags.
@ -510,7 +518,7 @@ This may lead to the following JSON data:
You see that the "belongsTo" relationships are detected and the foreign key value is replaced by the referenced object.
In case of "hasMany" and "hasAndBelongsToMany" the table name is used a new property on the object.
### Batch operations
#### Batch operations
When you want to create, read, update or delete you may specify multiple primary key values in the URL.
You also need to send an array instead of an object in the request body for create and update.
@ -574,7 +582,7 @@ of the batch failed due to an integrity violation:
The response status code will always be 424 (failed dependency) in case of any failure of one of the batch operations.
### Spatial support
#### Spatial support
For spatial support there is an extra set of filters that can be applied on geometry columns and that starting with an "s":
@ -592,7 +600,7 @@ For spatial support there is an extra set of filters that can be applied on geom
These filters are based on OGC standards and so is the WKT specification in which the geometry columns are represented.
#### GeoJSON
##### GeoJSON
The GeoJSON support is a read-only view on the tables and records in GeoJSON format. These requests are supported:
@ -615,7 +623,7 @@ The following Geometry types are supported by the GeoJSON implementation:
The GeoJSON functionality is enabled by default, but can be disabled using the "controllers" configuration.
## Middleware
### Middleware
You can enable the following middleware using the "middlewares" config parameter:
@ -709,7 +717,7 @@ If you don't specify these parameters in the configuration, then the default val
In the sections below you find more information on the built-in middleware.
### Authentication
#### Authentication
Currently there are three types of authentication supported. They all store the authenticated user in the `$_SESSION` super global.
This variable can be used in the authorization handlers to decide wether or not sombeody should have read or write access to certain tables, columns or records.
@ -723,7 +731,7 @@ The following overview shows the kinds of authentication middleware that you can
Below you find more information on each of the authentication types.
#### Database authentication
##### Database authentication
The database authentication middleware defines three new routes:
@ -752,7 +760,7 @@ users can freely add, modify or delete any account! The minimal configuration is
Note that this middleware uses session cookies and stores the logged in state on the server.
#### Basic authentication
##### Basic authentication
The Basic type supports a file (by default '.htpasswd') that holds the users and their (hashed) passwords separated by a colon (':').
When the passwords are entered in plain text they fill be automatically hashed.
@ -763,7 +771,7 @@ You need to send an "Authorization" header containing a base64 url encoded and c
This example sends the string "username1:password1".
#### JWT authentication
##### JWT authentication
The JWT type requires another (SSO/Identity) server to sign a token that contains claims.
Both servers share a secret so that they can either sign or verify that the signature is valid.
@ -786,7 +794,7 @@ This example sends the signed claims:
NB: The JWT implementation only supports the RSA and HMAC based algorithms.
##### Configure and test JWT authentication with Auth0
###### Configure and test JWT authentication with Auth0
First you need to create an account on [Auth0](https://auth0.com/auth/login).
Once logged in, you have to create an application (its type does not matter). Collect the `Domain`
@ -812,7 +820,7 @@ You can also change the `url` variable, used to test the API with authentication
[More info](https://auth0.com/docs/api-auth/tutorials/verify-access-token)
##### Configure and test JWT authentication with Firebase
###### Configure and test JWT authentication with Firebase
First you need to create a Firebase project on the [Firebase console](https://console.firebase.google.com/).
Add a web application to this project and grab the code snippet for later use.
@ -852,7 +860,7 @@ You can also change the `url` variable, used to test the API with authentication
[More info](https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library)
### Authorizing operations
#### Authorizing operations
The Authorization model acts on "operations". The most important ones are listed here:
@ -873,7 +881,7 @@ These operations can display or change the definition of the database, table or
This functionality is disabled by default and for good reason (be careful!).
Add the "columns" controller in the configuration to enable this functionality.
### Authorizing tables, columns and records
#### 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
and define a 'authorization.tableHandler' function that returns 'false' for these tables.
@ -905,7 +913,7 @@ 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
#### SQL GRANT authorization
You can alternatively use database permissons (SQL GRANT statements) to define the authorization model. In this case you
should not use the "authorization" middleware, but you do need to use the "reconnect" middleware. The handlers of the
@ -924,7 +932,7 @@ 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).
### Sanitizing input
#### Sanitizing input
By default all input is accepted and sent to the database. If you want to strip (certain) HTML tags before storing you may add
the 'sanitation' middleware and define a 'sanitation.handler' function that returns the adjusted value.
@ -935,7 +943,7 @@ 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
#### Type sanitation
If you enable the 'sanitation' middleware, then you (automatically) also enable type sanitation. When this is enabled you may:
@ -952,7 +960,7 @@ in which tables you want to apply type sanitation (defaults to 'all'). Example:
Here we enable the type sanitation for date and timestamp fields in the posts and comments tables.
### Validating input
#### 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
@ -982,7 +990,7 @@ 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
#### Type validations
If you enable the 'validation' middleware, then you (automatically) also enable type validation.
This includes the following error messages:
@ -1013,7 +1021,7 @@ Here we enable the type validation for date and timestamp fields in the posts an
NB: Types that are enabled will be checked for null values when the column is non-nullable.
### Multi-tenancy support
#### Multi-tenancy support
Two forms of multi-tenancy are supported:
@ -1022,7 +1030,7 @@ Two forms of multi-tenancy are supported:
Below is an explanation of the corresponding middlewares.
#### Multi-tenancy middleware
##### Multi-tenancy middleware
You may use the "multiTenancy" middleware when you have a single multi-tenant database.
If your tenants are identified by the "customer_id" column, then you can use the following handler:
@ -1036,7 +1044,7 @@ It also sets the column "customer_id" on "create" to "12" and removes the column
NB: You may want to retrieve the customer id from the session (the "$_SESSION" variable).
#### Reconnect middleware
##### Reconnect middleware
You may use the "reconnect" middleware when you have a separate database for each tenant.
If the tenant has it's own database named "customer_12", then you can use the following handler:
@ -1050,7 +1058,7 @@ to use the same credentials, then you should also implement the "usernameHandler
NB: You may want to retrieve the database name from the session (the "$_SESSION" variable).
### Prevent database scraping
#### Prevent database scraping
You may use the "joinLimits" and "pageLimits" middleware to prevent database scraping.
The "joinLimits" middleware limits the table depth, number of tables and number of records returned in a join operation.
@ -1068,7 +1076,7 @@ If you want to allow no more than 10 pages with a maximum of 25 records each, yo
NB: The maximum number of records is also applied when there is no page number specified in the request.
### Customization handlers
#### Customization handlers
You may use the "customization" middleware to modify request and response and implement any other functionality.
@ -1081,7 +1089,7 @@ 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
#### XML middleware
You may use the "xml" middleware to translate input and output from JSON to XML. This request:
@ -1111,11 +1119,11 @@ Outputs:
This functionality is disabled by default and must be enabled using the "middlewares" configuration setting.
### File uploads
#### 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).
## OpenAPI specification
### OpenAPI specification
On the "/openapi" end-point the OpenAPI 3.0 (formerly called "Swagger") specification is served.
It is a machine readable instant documentation of your API. To learn more, check out these links:
@ -1124,7 +1132,7 @@ It is a machine readable instant documentation of your API. To learn more, check
- [OpenAPI specification](https://swagger.io/specification/) is a manual for creating an OpenAPI specification.
- [Swagger Petstore](https://petstore.swagger.io/) is an example documentation that is generated using OpenAPI.
## Cache
### Cache
There are 4 cache engines that can be configured by the "cacheType" config parameter:
@ -1144,7 +1152,7 @@ The default engine has no dependencies and will use temporary files in the syste
You may use the "cachePath" config parameter to specify the file system path for the temporary files or
in case that you use a non-default "cacheType" the hostname (optionally with port) of the cache server.
## Types
### Types
These are the supported types with their length, category, JSON type and format:
@ -1167,19 +1175,19 @@ These are the supported types with their length, category, JSON type and format:
Note that geometry is a non-jdbc type and thus has limited support.
## Data types in JavaScript
### Data types in JavaScript
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.
### 64 bit integers
#### 64 bit integers
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
#### 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
### Errors
The following errors may be reported:
@ -1216,7 +1224,7 @@ The following JSON structure is used:
NB: Any non-error response will have status: 200 OK
## Tests
### Tests
I am testing mainly on Ubuntu and I have the following test setups:
@ -1230,7 +1238,7 @@ I am testing mainly on Ubuntu and I have the following test setups:
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.
### Running
#### Running
To run the functional tests locally you may run the following command:
@ -1239,7 +1247,7 @@ To run the functional tests locally you may run the following command:
This runs the functional tests from the "tests" directory. It uses the database dumps (fixtures) and
database configuration (config) from the corresponding subdirectories.
## Nginx config example
### Nginx config example
```
server {
@ -1270,7 +1278,7 @@ server {
}
```
### Docker tests
#### Docker tests
Install docker using the following commands and then logout and login for the changes to take effect:
@ -1380,7 +1388,7 @@ As you can see the "run.sh" script gives you access to a prompt in a chosen the
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
#### Docker image
There is a `Dockerfile` in the repository that is used to build an image at:
@ -1388,7 +1396,7 @@ There is a `Dockerfile` in the repository that is used to build an image at:
It will be automatically build on every release. The "latest" tag points to the last release.
### Docker compose
#### Docker compose
This repository also contains a `docker-compose.yml` file that you can install/build/run using:

151
docs/scheme_mld.svg Normal file
View file

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.0//EN'
'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd'>
<svg fill-opacity="1" xmlns:xlink="http://www.w3.org/1999/xlink" color-rendering="auto" color-interpolation="auto" text-rendering="auto" stroke="black" stroke-linecap="square" width="1150" stroke-miterlimit="10" shape-rendering="auto" stroke-opacity="1" fill="black" stroke-dasharray="none" font-weight="normal" stroke-width="1" viewBox="-20 70 1150 470" height="470" xmlns="http://www.w3.org/2000/svg" font-family="'Dialog'" font-style="normal" stroke-linejoin="miter" font-size="12px" stroke-dashoffset="0" image-rendering="auto"
><!--Generated by the Batik Graphics2D SVG Generator--><defs id="genericDefs"
/><g
><defs id="defs1"
><clipPath clipPathUnits="userSpaceOnUse" id="clipPath1"
><path d="M0 0 L2147483647 0 L2147483647 2147483647 L0 2147483647 L0 0 Z"
/></clipPath
><clipPath clipPathUnits="userSpaceOnUse" id="clipPath2"
><path d="M0 0 L0 120 L570 120 L570 0 Z"
/></clipPath
><clipPath clipPathUnits="userSpaceOnUse" id="clipPath3"
><path d="M0 0 L0 170 L560 170 L560 0 Z"
/></clipPath
><clipPath clipPathUnits="userSpaceOnUse" id="clipPath4"
><path d="M0 0 L0 110 L430 110 L430 0 Z"
/></clipPath
><clipPath clipPathUnits="userSpaceOnUse" id="clipPath5"
><path d="M0 0 L0 100 L70 100 L70 0 Z"
/></clipPath
><clipPath clipPathUnits="userSpaceOnUse" id="clipPath6"
><path d="M0 0 L0 320 L80 320 L80 0 Z"
/></clipPath
><clipPath clipPathUnits="userSpaceOnUse" id="clipPath7"
><path d="M0 0 L0 160 L90 160 L90 0 Z"
/></clipPath
><clipPath clipPathUnits="userSpaceOnUse" id="clipPath8"
><path d="M0 0 L0 140 L50 140 L50 0 Z"
/></clipPath
><clipPath clipPathUnits="userSpaceOnUse" id="clipPath9"
><path d="M0 0 L0 340 L80 340 L80 0 Z"
/></clipPath
></defs
><g fill="rgb(0,0,0)" fill-opacity="1" transform="translate(60,400)" stroke-opacity="1" stroke="rgb(0,0,0)"
><rect x="0.5" width="568.5" height="118.5" y="0.5" clip-path="url(#clipPath2)" stroke="none"
/></g
><g fill="white" transform="translate(60,400)" stroke="white"
><rect fill="none" x="0.5" width="568.5" height="118.5" y="0.5" clip-path="url(#clipPath2)"
/><text x="248" font-size="14px" y="18.1094" text-decoration="underline" clip-path="url(#clipPath2)" font-family="sans-serif" stroke="none" xml:space="preserve"
>Utilisateur</text
><path fill="none" d="M1 24.1094 L569 24.1094" clip-path="url(#clipPath2)"
/><text x="5" font-size="14px" y="39.2188" clip-path="url(#clipPath2)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- id : entier non null AUTO_INCREMENT</text
><text x="5" font-size="14px" y="55.3281" clip-path="url(#clipPath2)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- pseudonyme : chaine de caractère non null</text
><text x="5" font-size="14px" y="71.4375" clip-path="url(#clipPath2)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- mot de passe : chaine de caractère non null</text
></g
><g fill="rgb(0,0,0)" fill-opacity="1" transform="translate(60,90)" stroke-opacity="1" stroke="rgb(0,0,0)"
><rect x="0.5" width="568.5" height="118.5" y="0.5" clip-path="url(#clipPath2)" stroke="none"
/></g
><g fill="white" transform="translate(60,90)" stroke="white"
><rect fill="none" x="0.5" width="568.5" height="118.5" y="0.5" clip-path="url(#clipPath2)"
/><text x="253" font-size="14px" y="18.1094" text-decoration="underline" clip-path="url(#clipPath2)" font-family="sans-serif" stroke="none" xml:space="preserve"
>Prévision</text
><path fill="none" d="M1 24.1094 L569 24.1094" clip-path="url(#clipPath2)"
/><text x="5" font-size="14px" y="39.2188" clip-path="url(#clipPath2)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- user : entier non signé non null</text
><text x="5" font-size="14px" y="55.3281" clip-path="url(#clipPath2)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- ticket : entier non signé non null</text
><text x="5" font-size="14px" y="71.4375" clip-path="url(#clipPath2)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- estimation pessimiste : timestamp non null DEFAULT '0000-00-00 00:00:00'</text
><text x="5" font-size="14px" y="87.5469" clip-path="url(#clipPath2)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- estimation mitigé : timestamp non null DEFAULT '0000-00-00 00:00:00'</text
><text x="5" font-size="14px" y="103.6562" clip-path="url(#clipPath2)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- estimation optimiste : timestamp non null DEFAULT '0000-00-00 00:00:00'</text
></g
><g fill="rgb(0,0,0)" fill-opacity="1" transform="translate(60,220)" stroke-opacity="1" stroke="rgb(0,0,0)"
><rect x="0.5" width="558.5" height="168.5" y="0.5" clip-path="url(#clipPath3)" stroke="none"
/></g
><g fill="white" transform="translate(60,220)" stroke="white"
><rect fill="none" x="0.5" width="558.5" height="168.5" y="0.5" clip-path="url(#clipPath3)"
/><text x="258" font-size="14px" y="18.1094" text-decoration="underline" clip-path="url(#clipPath3)" font-family="sans-serif" stroke="none" xml:space="preserve"
>Ticket</text
><path fill="none" d="M1 24.1094 L559 24.1094" clip-path="url(#clipPath3)"
/><text x="5" font-size="14px" y="39.2188" clip-path="url(#clipPath3)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- id : entier non signé non null AUTO_INCREMENT,</text
><text x="5" font-size="14px" y="55.3281" clip-path="url(#clipPath3)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- nom : chaîne de caractère</text
><text x="5" font-size="14px" y="71.4375" clip-path="url(#clipPath3)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- date de création : timestamp non null DEFAULT current_timestamp(),</text
><text x="5" font-size="14px" y="87.5469" clip-path="url(#clipPath3)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- date de modification : timestamp </text
><text x="5" font-size="14px" y="103.6562" clip-path="url(#clipPath3)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- project : entier non signé </text
><text x="5" font-size="14px" y="119.7656" clip-path="url(#clipPath3)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- temps passé : timestamp </text
><text x="5" font-size="14px" y="135.875" clip-path="url(#clipPath3)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- description : chaîne de caractère</text
><text x="5" font-size="14px" y="151.9844" clip-path="url(#clipPath3)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- auteurice : entier non signé</text
><text x="5" font-size="14px" y="168.0938" clip-path="url(#clipPath3)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- ticket :</text
></g
><g fill="rgb(0,0,0)" fill-opacity="1" transform="translate(680,90)" stroke-opacity="1" stroke="rgb(0,0,0)"
><rect x="0.5" width="428.5" height="108.5" y="0.5" clip-path="url(#clipPath4)" stroke="none"
/></g
><g fill="white" transform="translate(680,90)" stroke="white"
><rect fill="none" x="0.5" width="428.5" height="108.5" y="0.5" clip-path="url(#clipPath4)"
/><text x="174" font-size="14px" y="18.1094" text-decoration="underline" clip-path="url(#clipPath4)" font-family="sans-serif" stroke="none" xml:space="preserve"
>Assignation</text
><path fill="none" d="M1 24.1094 L429 24.1094" clip-path="url(#clipPath4)"
/><text x="5" font-size="14px" y="39.2188" clip-path="url(#clipPath4)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- ticket : entier non signé non null</text
><text x="5" font-size="14px" y="55.3281" clip-path="url(#clipPath4)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- utilisateur : entier non signé non null</text
><text x="5" font-size="14px" y="71.4375" clip-path="url(#clipPath4)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- assignation : timestamp DEFAULT CURRENT_TIMESTAMP</text
><text x="5" font-size="14px" y="87.5469" clip-path="url(#clipPath4)" font-family="sans-serif" stroke="none" xml:space="preserve"
>- tempsPassé : entier</text
></g
><g transform="translate(10,240)"
><path fill="none" d="M50.5 10.5 L10.5 10.5" clip-path="url(#clipPath5)"
/><path fill="none" d="M10.5 10.5 L10.5 80.5" clip-path="url(#clipPath5)"
/><path fill="none" d="M10.5 80.5 L50.5 80.5" clip-path="url(#clipPath5)"
/><path fill="white" d="M39.2417 4 L50.5 10.5 L39.2417 17 L39.2417 4" clip-path="url(#clipPath5)" stroke="none"
/><path fill="none" d="M39.2417 4 L50.5 10.5 L39.2417 17 L39.2417 4" clip-path="url(#clipPath5)"
/></g
><g transform="translate(620,130)"
><path fill="none" d="M10.5 300.5 L40.5 300.5" clip-path="url(#clipPath6)"
/><path fill="none" d="M40.5 300.5 L40.5 10.5" clip-path="url(#clipPath6)"
/><path fill="none" d="M40.5 10.5 L60.5 10.5" clip-path="url(#clipPath6)"
/><path fill="white" d="M21.7583 307 L10.5 300.5 L21.7583 294 L21.7583 307" clip-path="url(#clipPath6)" stroke="none"
/><path fill="none" d="M21.7583 307 L10.5 300.5 L21.7583 294 L21.7583 307" clip-path="url(#clipPath6)"
/></g
><g transform="translate(610,120)"
><path fill="none" d="M10.5 140.5 L30.5 140.5" clip-path="url(#clipPath7)"
/><path fill="none" d="M30.5 140.5 L30.5 10.5" clip-path="url(#clipPath7)"
/><path fill="none" d="M30.5 10.5 L70.5 10.5" clip-path="url(#clipPath7)"
/><path fill="white" d="M21.7583 147 L10.5 140.5 L21.7583 134 L21.7583 147" clip-path="url(#clipPath7)" stroke="none"
/><path fill="none" d="M21.7583 147 L10.5 140.5 L21.7583 134 L21.7583 147" clip-path="url(#clipPath7)"
/></g
><g transform="translate(30,130)"
><path fill="none" d="M30.5 120.5 L10.5 120.5" clip-path="url(#clipPath8)"
/><path fill="none" d="M10.5 120.5 L10.5 10.5" clip-path="url(#clipPath8)"
/><path fill="none" d="M10.5 10.5 L30.5 10.5" clip-path="url(#clipPath8)"
/><path fill="white" d="M19.2417 114 L30.5 120.5 L19.2417 127 L19.2417 114" clip-path="url(#clipPath8)" stroke="none"
/><path fill="none" d="M19.2417 114 L30.5 120.5 L19.2417 127 L19.2417 114" clip-path="url(#clipPath8)"
/></g
><g transform="translate(0,110)"
><path fill="none" d="M60.5 320.5 L10.5 320.5" clip-path="url(#clipPath9)"
/><path fill="none" d="M10.5 320.5 L10.5 10.5" clip-path="url(#clipPath9)"
/><path fill="none" d="M10.5 10.5 L60.5 10.5" clip-path="url(#clipPath9)"
/><path fill="white" d="M49.2417 314 L60.5 320.5 L49.2417 327 L49.2417 314" clip-path="url(#clipPath9)" stroke="none"
/><path fill="none" d="M49.2417 314 L60.5 320.5 L49.2417 327 L49.2417 314" clip-path="url(#clipPath9)"
/></g
></g
></svg
>

After

Width:  |  Height:  |  Size: 11 KiB