Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c758dc9317 | |||
| 394a85ef45 | |||
| 1f32935a29 | |||
| 951fe36da3 | |||
| d258bcc919 | |||
| fab8efd780 | |||
| 746b06e317 | |||
| 3fa131e1b2 | |||
| c0fd7b3fe5 | |||
| dab07e55ec | |||
| a10e864ba3 | |||
| 9d45bb5ceb | |||
| f1e6e92f46 | |||
| 2d3f8bfdd4 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/composer.lock
|
||||||
|
/vendor
|
||||||
46
AGENTS.md
Normal file
46
AGENTS.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Project overview
|
||||||
|
APIlite is a lightweight PHP library. A class extends `TPsoft\APIlite\APIlite`, public methods become API actions, requests are processed automatically, and responses are returned as JSON.
|
||||||
|
|
||||||
|
The project also generates frontend clients:
|
||||||
|
- JavaScript client: `format=javascript` (`.js`, includes `bearerSet()` helper backed by `localStorage`)
|
||||||
|
- TypeScript client: `format=typescript` (`.ts`, real typed output, includes `bearerSet()` helper backed by `localStorage`)
|
||||||
|
|
||||||
|
The HTML help page is also interactive:
|
||||||
|
- HTML help: `format=html` includes endpoint docs, a built-in request tester, and optional Bearer token support stored in browser `localStorage`
|
||||||
|
|
||||||
|
## Core files
|
||||||
|
- `src/APIlite.php`: runtime, routing, docs JSON/HTML/client generation
|
||||||
|
- `src/help.tpl.php`: HTML documentation template
|
||||||
|
- `src/javascript.tpl.php`: JavaScript client template
|
||||||
|
- `src/typescript.tpl.php`: TypeScript client template
|
||||||
|
- `bin/apilite-files`: helper script for bootstrapping files in consumer projects
|
||||||
|
|
||||||
|
## Working rules
|
||||||
|
- Keep API behavior backward compatible unless explicitly requested.
|
||||||
|
- Maintain both generated clients:
|
||||||
|
- JavaScript stays plain JS.
|
||||||
|
- TypeScript stays typed (interfaces + typed method signatures).
|
||||||
|
- Bearer token behavior stays aligned between JavaScript and TypeScript clients (`bearerSet()`, `apilite_bearer_token`, automatic `Authorization: Bearer ...` header).
|
||||||
|
- Keep the HTML help page usable both as documentation and as a lightweight in-browser tester.
|
||||||
|
- If you change output field names in JSON help, update README and templates consistently.
|
||||||
|
- If you change request metadata or tester behavior, update `README.md` and keep `src/help.tpl.php` aligned with the actual runtime request model.
|
||||||
|
- Prefer small, focused changes over broad rewrites.
|
||||||
|
|
||||||
|
## Verification checklist
|
||||||
|
- Run syntax checks on edited PHP files:
|
||||||
|
- `php -l src/APIlite.php`
|
||||||
|
- `php -l src/help.tpl.php`
|
||||||
|
- `php -l src/javascript.tpl.php`
|
||||||
|
- `php -l src/typescript.tpl.php`
|
||||||
|
- `php -l bin/apilite-files`
|
||||||
|
- If HTML help changes, also render it once:
|
||||||
|
- `php test/APIcalculator.php --html`
|
||||||
|
- If client generation changes, test with:
|
||||||
|
- `php test/APIcalculator.php --javascript`
|
||||||
|
- `php test/APIcalculator.php --typescript`
|
||||||
|
|
||||||
|
## Documentation rule
|
||||||
|
When output format names/flags/URLs change, update `README.md` in the same change set.
|
||||||
|
When Bearer token helper/storage behavior changes, update `README.md` and keep JS/TS client docs aligned with the actual generated templates.
|
||||||
151
README.md
151
README.md
@ -2,6 +2,44 @@
|
|||||||
|
|
||||||
A set of tools to simplify the work of creating backend APIs for your frontend projects. Includes tutorials, patterns and practical examples for creating projects based on REST APIs.
|
A set of tools to simplify the work of creating backend APIs for your frontend projects. Includes tutorials, patterns and practical examples for creating projects based on REST APIs.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Download source code and add to your project
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/APIlite/src/APIlite.php';
|
||||||
|
```
|
||||||
|
|
||||||
|
or use composer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require tpsoft/apilite
|
||||||
|
```
|
||||||
|
|
||||||
|
and add to your project
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
class YourAPI extends \TPsoft\APIlite\APIlite {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* My first method for greetings.
|
||||||
|
*
|
||||||
|
* @param string $name Your name
|
||||||
|
* @param int $age Your age
|
||||||
|
* @return string Greetings
|
||||||
|
*/
|
||||||
|
public function myFirstMethod(string $name, int $age): string
|
||||||
|
{
|
||||||
|
return 'Hi, I`m '.$name.' and I`m '.$age.' years old.';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Basic usage
|
## Basic usage
|
||||||
|
|
||||||
For example, we create an API for calculator. So we create class `APIcalculator` and store in file `test/APIcalculator.php` where we defined each actions for API as public method.
|
For example, we create an API for calculator. So we create class `APIcalculator` and store in file `test/APIcalculator.php` where we defined each actions for API as public method.
|
||||||
@ -94,6 +132,7 @@ When you run this subfile through the webserver, you will see the JSON documenta
|
|||||||
{
|
{
|
||||||
"name": "APIcalculator",
|
"name": "APIcalculator",
|
||||||
"html_version": "http://localhost/APIlite/test/APIcalculator.php?format=html",
|
"html_version": "http://localhost/APIlite/test/APIcalculator.php?format=html",
|
||||||
|
"javascript_version": "http://localhost/APIlite/test/APIcalculator.php?format=javascript",
|
||||||
"typescript_version": "http://localhost/APIlite/test/APIcalculator.php?format=typescript",
|
"typescript_version": "http://localhost/APIlite/test/APIcalculator.php?format=typescript",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
@ -192,84 +231,54 @@ and there is also an HTML version available
|
|||||||
|
|
||||||
<img src="test/Example.APIcalculator.png" />
|
<img src="test/Example.APIcalculator.png" />
|
||||||
|
|
||||||
To connect to the API from TypeScript (e.g. Vue application) it is possible to download the backend script
|
The HTML documentation page also includes a built-in interactive tester. Each endpoint can be expanded with `Try it`, filled directly in the browser, and executed against the current API endpoint. If needed, you can set a shared Bearer token at the top of the page. The token is stored in `localStorage` and is sent as `Authorization: Bearer ...` for test requests made from that page.
|
||||||
|
|
||||||
```ts
|
You can also download generated frontend clients:
|
||||||
/**
|
|
||||||
* Generated by APIlite
|
|
||||||
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
|
||||||
*
|
|
||||||
* 2025-05-28 15:44:07 */
|
|
||||||
|
|
||||||
export const backend = {
|
* JavaScript: `?format=javascript`
|
||||||
endpont: window.location.origin + "APIcalculator.php",
|
* TypeScript (typed for Vue/TS projects): `?format=typescript`
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
Both generated clients support a shared Bearer token helper. Call `bearerSet(token)` once, the token is stored in `localStorage` under `apilite_bearer_token`, and subsequent requests automatically send `Authorization: Bearer ...`.
|
||||||
* General API call
|
|
||||||
*/
|
|
||||||
call(method, data, callback) {
|
|
||||||
var xhttp = new XMLHttpRequest();
|
|
||||||
xhttp.withCredentials = true;
|
|
||||||
xhttp.onreadystatechange = function() {
|
|
||||||
if (this.readyState == 4) {
|
|
||||||
if (this.status == 200) {
|
|
||||||
if (callback != null) callback(JSON.parse(this.responseText));
|
|
||||||
} else {
|
|
||||||
if (callback != null) callback({'status': 'ERROR', 'message': 'HTTP STATUS ' + this.status});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var form_data = new FormData();
|
|
||||||
Object.keys(data).forEach(key => {
|
|
||||||
let val = data[key];
|
|
||||||
if (typeof val == 'object') val = JSON.stringify(val);
|
|
||||||
form_data.append(key, val);
|
|
||||||
});
|
|
||||||
xhttp.open('POST', this.endpont + '?action=' + method);
|
|
||||||
xhttp.send(form_data);
|
|
||||||
},
|
|
||||||
|
|
||||||
callPromise(method, data) {
|
JavaScript usage example:
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.call(method, data, function(response) {
|
|
||||||
if (response.status == 'OK') {
|
|
||||||
resolve(response.data);
|
|
||||||
} else {
|
|
||||||
reject(response.msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
```js
|
||||||
* API actions
|
import backend from './backend.js';
|
||||||
*/
|
|
||||||
help() {
|
|
||||||
return this.callPromise('__HELP__', {});
|
|
||||||
},
|
|
||||||
|
|
||||||
add(a, b) {
|
backend.bearerSet('your-access-token');
|
||||||
return this.callPromise('add', {a: a, b: b});
|
|
||||||
},
|
|
||||||
|
|
||||||
subtract(a, b) {
|
|
||||||
return this.callPromise('subtract', {a: a, b: b});
|
|
||||||
},
|
|
||||||
|
|
||||||
multiply(a, b) {
|
|
||||||
return this.callPromise('multiply', {a: a, b: b});
|
|
||||||
},
|
|
||||||
|
|
||||||
divide(a, b) {
|
|
||||||
return this.callPromise('divide', {a: a, b: b});
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
backend.add(1, 2).then((response) => {
|
||||||
|
console.log(response.data);
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
These outputs can also be generated in the command line as follows
|
TypeScript usage example (Vue + TS):
|
||||||
|
|
||||||
* for HTML`$> php APIcalculator.php --html`
|
```ts
|
||||||
* for TypeScript`$> php APIcalculator.php --typescript`
|
import backend from './backend';
|
||||||
|
|
||||||
|
backend.bearerSet('your-access-token');
|
||||||
|
|
||||||
|
backend.add(1, 2).then((response) => {
|
||||||
|
console.log(response.data); // typed value based on PHP return type
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If a method return type is a PHP class with public properties, JSON help now also includes `return_structure`, and the generated TypeScript client maps that class to an object shape based on those public properties.
|
||||||
|
|
||||||
|
These outputs can also be generated in command line:
|
||||||
|
|
||||||
|
* HTML: `$> php APIcalculator.php --html`
|
||||||
|
* JavaScript: `$> php APIcalculator.php --javascript > backend.js` (`--js` alias is available)
|
||||||
|
* TypeScript: `$> php APIcalculator.php --typescript > backend.ts`
|
||||||
|
|
||||||
|
## HTML help tester notes
|
||||||
|
|
||||||
|
The built-in HTML tester currently follows the default APIlite request model:
|
||||||
|
|
||||||
|
* request method is `POST`
|
||||||
|
* endpoint action is sent as query parameter `action`
|
||||||
|
* request fields are sent as top-level form fields
|
||||||
|
* object and array field values are serialized as JSON strings
|
||||||
|
|
||||||
|
Because the current metadata does not explicitly distinguish path params, query params and request body schemas, the HTML tester renders the parts that are reliably available today and keeps the rest minimal.
|
||||||
|
|||||||
129
bin/apilite-files
Normal file
129
bin/apilite-files
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
include $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
echo "ℹ️ APIlite files\n\n";
|
||||||
|
|
||||||
|
$project_root = findProjectRoot(__DIR__);
|
||||||
|
|
||||||
|
if (!$project_root) {
|
||||||
|
echo "⛔ Project root not found\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
$composer_arr = json_decode(file_get_contents($project_root . '/composer.json'), true);
|
||||||
|
|
||||||
|
echo "Project root: {$project_root}\n";
|
||||||
|
$source_dirs = array_values($composer_arr['autoload']['psr-4']);
|
||||||
|
foreach ($source_dirs as $index => $source_dir) {
|
||||||
|
echo "➡️ [$index]: $project_root/$source_dir\n";
|
||||||
|
}
|
||||||
|
$selected = readline("❔ Select source dir [default: 0]: ");
|
||||||
|
$selected = empty($selected) ? 0 : $selected;
|
||||||
|
if (!isset($source_dirs[$selected])) {
|
||||||
|
echo "⛔ Invalid selection\n";
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
$source_dir = $project_root . '/' . $source_dirs[$selected];
|
||||||
|
$source_dir = rtrim($source_dir, '/') . '/';
|
||||||
|
$namespaces = array_keys($composer_arr['autoload']['psr-4']);
|
||||||
|
$namespace = $namespaces[$selected];
|
||||||
|
echo "✔️ Source dir: {$source_dir} with namespace: {$namespace}\n";
|
||||||
|
|
||||||
|
// Copy files
|
||||||
|
echo "ℹ️ Copy files\n";
|
||||||
|
|
||||||
|
copyFile($source_dir . 'API.php', '<' . '?php
|
||||||
|
|
||||||
|
namespace ' . rtrim($namespace, '\\') . ';
|
||||||
|
|
||||||
|
use TPsoft\APIlite\APIlite;
|
||||||
|
|
||||||
|
class API extends APIlite {
|
||||||
|
|
||||||
|
}
|
||||||
|
');
|
||||||
|
|
||||||
|
copyFile($project_root . '/scripts/buildTypeScript.php', '<' . '?php
|
||||||
|
|
||||||
|
require __DIR__ . \'/../vendor/autoload.php\';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
$backend_api = new TPsoft\BugreportBackend\API(\'typescript\', \'import.meta.env.VITE_BACKENDAPI_URL\', \'backend\');
|
||||||
|
|
||||||
|
$output = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
|
|
||||||
|
$ts_path = realpath(__DIR__ . \'/../../frontend/src\').\'/backend.ts\';
|
||||||
|
$suc = file_put_contents($ts_path, $output);
|
||||||
|
if ($suc === false) {
|
||||||
|
echo "✗ TypeScript store into file failed\n";
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ TypeScript backend script created\n";
|
||||||
|
');
|
||||||
|
|
||||||
|
copyFile($project_root . '/public/API.php', '<' . '?php
|
||||||
|
|
||||||
|
require_once __DIR__.\'/../src/API.php\';
|
||||||
|
new \\' . $namespace . 'API();
|
||||||
|
');
|
||||||
|
|
||||||
|
// Change composer.json
|
||||||
|
if (!isset($composer_arr['scripts']['build'])) {
|
||||||
|
$composer_arr['scripts']['build'] = 'php scripts/buildTypeScript.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$composer_json = json_encode($composer_arr, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
$composer_json = preg_replace_callback(
|
||||||
|
'/^( +)/m',
|
||||||
|
fn($m) => str_repeat("\t", intdiv(strlen($m[1]), 4)),
|
||||||
|
$composer_json
|
||||||
|
);
|
||||||
|
$suc = file_put_contents($project_root . '/composer.json', $composer_json);
|
||||||
|
if ($suc) {
|
||||||
|
echo "✅ File changed: $project_root/composer.json\n";
|
||||||
|
} else {
|
||||||
|
echo "🔴 Failed to change file: $project_root/composer.json\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions
|
||||||
|
*/
|
||||||
|
function findProjectRoot(string $startDir): ?string
|
||||||
|
{
|
||||||
|
$dir = realpath($startDir);
|
||||||
|
if (!$dir) return null;
|
||||||
|
while ($dir && $dir !== dirname($dir)) {
|
||||||
|
if (is_file($dir . '/vendor/autoload.php')) {
|
||||||
|
return $dir;
|
||||||
|
}
|
||||||
|
$dir = dirname($dir);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyFile(string $destination, string $content): bool
|
||||||
|
{
|
||||||
|
$dir = dirname($destination);
|
||||||
|
if (!file_exists($dir)) {
|
||||||
|
mkdir($dir, 0777, true);
|
||||||
|
}
|
||||||
|
if (file_exists($destination)) {
|
||||||
|
echo "⚠️ File already exists: $destination\n";
|
||||||
|
$confirm = readline("❔ Overwrite? (y - yes, other - no): ");
|
||||||
|
if ($confirm != 'y') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$suc = file_put_contents($destination, $content) !== false;
|
||||||
|
if ($suc) {
|
||||||
|
echo "✅ File created: $destination\n";
|
||||||
|
} else {
|
||||||
|
echo "🔴 Failed to create file: $destination\n";
|
||||||
|
}
|
||||||
|
return $suc;
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@
|
|||||||
"rest",
|
"rest",
|
||||||
"json",
|
"json",
|
||||||
"php",
|
"php",
|
||||||
|
"javascript",
|
||||||
"typescript"
|
"typescript"
|
||||||
],
|
],
|
||||||
"authors": [
|
"authors": [
|
||||||
@ -32,5 +33,8 @@
|
|||||||
"psr-4": {
|
"psr-4": {
|
||||||
"TPsoft\\APIlite\\": "src/"
|
"TPsoft\\APIlite\\": "src/"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/apilite-files"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
176
src/APIlite.php
176
src/APIlite.php
@ -21,10 +21,11 @@ class APIlite
|
|||||||
private string $endpoint = '';
|
private string $endpoint = '';
|
||||||
private $methods = array();
|
private $methods = array();
|
||||||
|
|
||||||
public function __construct(?string $format = null, ?string $endpoint = null)
|
public function __construct(?string $format = null, ?string $endpoint = null, ?string $apiName = null)
|
||||||
{
|
{
|
||||||
register_shutdown_function(array($this, '_shutdownHandler'));
|
register_shutdown_function(array($this, '_shutdownHandler'));
|
||||||
$this->endpoint = $endpoint ?? $this->getCurrentUrl();
|
$this->endpoint = $endpoint ?? $this->getCurrentUrl();
|
||||||
|
$this->apiName = $apiName ?? '';
|
||||||
$this->analyzeClass();
|
$this->analyzeClass();
|
||||||
if (isset($_REQUEST['action'])) {
|
if (isset($_REQUEST['action'])) {
|
||||||
$this->doAction($_REQUEST['action']);
|
$this->doAction($_REQUEST['action']);
|
||||||
@ -38,6 +39,9 @@ class APIlite
|
|||||||
if (in_array('--html', $switches)) {
|
if (in_array('--html', $switches)) {
|
||||||
$format = 'html';
|
$format = 'html';
|
||||||
}
|
}
|
||||||
|
if (in_array('--javascript', $switches) || in_array('--js', $switches)) {
|
||||||
|
$format = 'javascript';
|
||||||
|
}
|
||||||
if (in_array('--typescript', $switches)) {
|
if (in_array('--typescript', $switches)) {
|
||||||
$format = 'typescript';
|
$format = 'typescript';
|
||||||
}
|
}
|
||||||
@ -46,6 +50,10 @@ class APIlite
|
|||||||
case 'html':
|
case 'html':
|
||||||
$this->printHelpHTML();
|
$this->printHelpHTML();
|
||||||
break;
|
break;
|
||||||
|
case 'javascript':
|
||||||
|
case 'js':
|
||||||
|
$this->printHelpJavascript();
|
||||||
|
break;
|
||||||
case 'typescript':
|
case 'typescript':
|
||||||
$this->printHelpTypescript();
|
$this->printHelpTypescript();
|
||||||
break;
|
break;
|
||||||
@ -67,7 +75,9 @@ class APIlite
|
|||||||
private function analyzeClass(): bool
|
private function analyzeClass(): bool
|
||||||
{
|
{
|
||||||
$refClass = new \ReflectionClass($this);
|
$refClass = new \ReflectionClass($this);
|
||||||
$this->apiName = $refClass->getShortName();
|
if (strlen($this->apiName) <= 0) {
|
||||||
|
$this->apiName = $refClass->getShortName();
|
||||||
|
}
|
||||||
$this->methods = array();
|
$this->methods = array();
|
||||||
foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $ref_method) {
|
foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $ref_method) {
|
||||||
$method_name = $ref_method->getName();
|
$method_name = $ref_method->getName();
|
||||||
@ -77,7 +87,8 @@ class APIlite
|
|||||||
'doc' => null,
|
'doc' => null,
|
||||||
'description' => null,
|
'description' => null,
|
||||||
'params' => array(),
|
'params' => array(),
|
||||||
'return' => null
|
'return' => null,
|
||||||
|
'return_structure' => null
|
||||||
);
|
);
|
||||||
$docComment = $ref_method->getDocComment();
|
$docComment = $ref_method->getDocComment();
|
||||||
if ($docComment) {
|
if ($docComment) {
|
||||||
@ -105,27 +116,129 @@ class APIlite
|
|||||||
}
|
}
|
||||||
if ($ref_method->hasReturnType()) {
|
if ($ref_method->hasReturnType()) {
|
||||||
$ref_type = $ref_method->getReturnType();
|
$ref_type = $ref_method->getReturnType();
|
||||||
if ($ref_type instanceof \ReflectionNamedType) {
|
$method['return'] = $this->reflectionTypeToNames($ref_type, $ref_method->getDeclaringClass());
|
||||||
$method['return'] = $ref_type->getName();
|
$method['return_structure'] = $this->reflectionTypeToStructure($ref_type, $ref_method->getDeclaringClass());
|
||||||
}
|
|
||||||
if ($ref_type instanceof \ReflectionUnionType
|
|
||||||
|| $ref_type instanceof \ReflectionIntersectionType )
|
|
||||||
{
|
|
||||||
$types = $ref_type->getTypes();
|
|
||||||
$method['return'] = [];
|
|
||||||
foreach ($types as $type) {
|
|
||||||
if ($type instanceof \ReflectionNamedType) {
|
|
||||||
$method['return'][] = $type->getName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$this->methods[] = $method;
|
$this->methods[] = $method;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function reflectionTypeToNames(?\ReflectionType $ref_type, ?\ReflectionClass $context_class = null): string|array|null
|
||||||
|
{
|
||||||
|
if ($ref_type instanceof \ReflectionNamedType) {
|
||||||
|
$type_name = $this->resolveTypeName($ref_type->getName(), $context_class);
|
||||||
|
if ($ref_type->allowsNull() && strtolower($type_name) !== 'null' && strtolower($type_name) !== 'mixed') {
|
||||||
|
return array($type_name, 'null');
|
||||||
|
}
|
||||||
|
return $type_name;
|
||||||
|
}
|
||||||
|
if ($ref_type instanceof \ReflectionUnionType || $ref_type instanceof \ReflectionIntersectionType) {
|
||||||
|
$types = array();
|
||||||
|
foreach ($ref_type->getTypes() as $type) {
|
||||||
|
if (!$type instanceof \ReflectionNamedType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$one_type = $this->reflectionTypeToNames($type, $context_class);
|
||||||
|
if (is_array($one_type)) {
|
||||||
|
$types = array_merge($types, $one_type);
|
||||||
|
} else if (is_string($one_type)) {
|
||||||
|
$types[] = $one_type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$types = array_values(array_unique($types));
|
||||||
|
return empty($types) ? null : $types;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function reflectionTypeToStructure(?\ReflectionType $ref_type, ?\ReflectionClass $context_class = null, array $visited = array()): array|null
|
||||||
|
{
|
||||||
|
if ($ref_type instanceof \ReflectionNamedType) {
|
||||||
|
return $this->classTypeStructure(
|
||||||
|
$this->resolveTypeName($ref_type->getName(), $context_class),
|
||||||
|
$visited
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($ref_type instanceof \ReflectionUnionType || $ref_type instanceof \ReflectionIntersectionType) {
|
||||||
|
$structures = array();
|
||||||
|
foreach ($ref_type->getTypes() as $type) {
|
||||||
|
if (!$type instanceof \ReflectionNamedType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$structure = $this->reflectionTypeToStructure($type, $context_class, $visited);
|
||||||
|
if (!is_null($structure)) {
|
||||||
|
$structures[] = $structure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count($structures) > 0 ? $structures : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function classTypeStructure(string $type_name, array $visited = array()): array|null
|
||||||
|
{
|
||||||
|
$type_name = ltrim($type_name, '\\');
|
||||||
|
if ($this->isBuiltinTypeName($type_name) || !class_exists($type_name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (in_array($type_name, $visited, true)) {
|
||||||
|
return array(
|
||||||
|
'type' => $type_name,
|
||||||
|
'recursive' => true,
|
||||||
|
'properties' => array()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$visited[] = $type_name;
|
||||||
|
$refClass = new \ReflectionClass($type_name);
|
||||||
|
$properties = array();
|
||||||
|
foreach ($refClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $ref_property) {
|
||||||
|
if ($ref_property->isStatic()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$property_type = $ref_property->getType();
|
||||||
|
$properties[] = array(
|
||||||
|
'name' => $ref_property->getName(),
|
||||||
|
'type' => $this->reflectionTypeToNames($property_type, $refClass),
|
||||||
|
'nullable' => is_null($property_type) ? true : $property_type->allowsNull(),
|
||||||
|
'type_structure' => $this->reflectionTypeToStructure($property_type, $refClass, $visited)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'type' => $type_name,
|
||||||
|
'properties' => $properties
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveTypeName(string $type_name, ?\ReflectionClass $context_class = null): string
|
||||||
|
{
|
||||||
|
$type_name = ltrim($type_name, '\\');
|
||||||
|
if (is_null($context_class)) {
|
||||||
|
return $type_name;
|
||||||
|
}
|
||||||
|
switch (strtolower($type_name)) {
|
||||||
|
case 'self':
|
||||||
|
case 'static':
|
||||||
|
return $context_class->getName();
|
||||||
|
case 'parent':
|
||||||
|
$parent = $context_class->getParentClass();
|
||||||
|
return $parent ? $parent->getName() : $type_name;
|
||||||
|
default:
|
||||||
|
return $type_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isBuiltinTypeName(string $type_name): bool
|
||||||
|
{
|
||||||
|
return in_array(strtolower(ltrim($type_name, '\\')), array(
|
||||||
|
'array', 'bool', 'boolean', 'callable', 'closure', 'false', 'float', 'int', 'integer',
|
||||||
|
'iterable', 'mixed', 'never', 'null', 'object', 'resource', 'scalar', 'string',
|
||||||
|
'true', 'void'
|
||||||
|
), true);
|
||||||
|
}
|
||||||
|
|
||||||
private function parseDescription(string $doc): string
|
private function parseDescription(string $doc): string
|
||||||
{
|
{
|
||||||
$lines = explode("\n", $doc);
|
$lines = explode("\n", $doc);
|
||||||
@ -186,6 +299,7 @@ class APIlite
|
|||||||
$this->response(array(
|
$this->response(array(
|
||||||
'name' => $this->apiName,
|
'name' => $this->apiName,
|
||||||
'html_version' => $this->getCurrentUrl().'?format=html',
|
'html_version' => $this->getCurrentUrl().'?format=html',
|
||||||
|
'javascript_version' => $this->getCurrentUrl().'?format=javascript',
|
||||||
'typescript_version' => $this->getCurrentUrl().'?format=typescript',
|
'typescript_version' => $this->getCurrentUrl().'?format=typescript',
|
||||||
'actions' => $this->methods
|
'actions' => $this->methods
|
||||||
));
|
));
|
||||||
@ -196,20 +310,36 @@ class APIlite
|
|||||||
include __DIR__ . '/help.tpl.php';
|
include __DIR__ . '/help.tpl.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function printHelpTypescript(): void
|
private function printHelpJavascript(): void
|
||||||
{
|
{
|
||||||
ob_clean();
|
ob_clean();
|
||||||
header('Content-Type: application/javascript');
|
header('Content-Type: application/javascript');
|
||||||
header('Content-Disposition: attachment; filename="' . $this->apiName . '.js"');
|
header('Content-Disposition: attachment; filename="' . $this->apiName . '.js"');
|
||||||
|
include __DIR__ . '/javascript.tpl.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function printHelpTypescript(): void
|
||||||
|
{
|
||||||
|
ob_clean();
|
||||||
|
header('Content-Type: application/typescript');
|
||||||
|
header('Content-Disposition: attachment; filename="' . $this->apiName . '.ts"');
|
||||||
include __DIR__ . '/typescript.tpl.php';
|
include __DIR__ . '/typescript.tpl.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getCurrentUrl(): string {
|
private function getCurrentUrl(): string {
|
||||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ||
|
$https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||||
$_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
|
$serverPort = isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : null;
|
||||||
$host = $_SERVER['HTTP_HOST'];
|
$protocol = ($https || $serverPort === 443) ? 'https://' : 'http://';
|
||||||
$uri = $_SERVER['REQUEST_URI'];
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? ($_SERVER['SCRIPT_NAME'] ?? '/');
|
||||||
$path = parse_url($uri, PHP_URL_PATH);
|
$path = parse_url($uri, PHP_URL_PATH);
|
||||||
|
if (!is_string($path) || $path === '') {
|
||||||
|
$path = '/';
|
||||||
|
}
|
||||||
|
$path = str_replace('\\', '/', $path);
|
||||||
|
if (substr($path, 0, 1) !== '/') {
|
||||||
|
$path = '/' . ltrim($path, '/');
|
||||||
|
}
|
||||||
return $protocol . $host . $path;
|
return $protocol . $host . $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1115
src/help.tpl.php
1115
src/help.tpl.php
File diff suppressed because it is too large
Load Diff
121
src/javascript.tpl.php
Normal file
121
src/javascript.tpl.php
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Generated by APIlite
|
||||||
|
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
||||||
|
*
|
||||||
|
* <?php echo date('Y-m-d H:i:s'); ?>
|
||||||
|
*/
|
||||||
|
|
||||||
|
class <?php echo $this->apiName; ?> {
|
||||||
|
endpoint = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
|
||||||
|
bearerStorageKey = 'apilite_bearer_token';
|
||||||
|
|
||||||
|
normalizeBearerToken(token) {
|
||||||
|
if (typeof token !== 'string') return null;
|
||||||
|
token = token.trim();
|
||||||
|
return token === '' ? null : token;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStorage() {
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined' && window.localStorage) return window.localStorage;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBearerToken() {
|
||||||
|
var storage = this.getStorage();
|
||||||
|
if (storage == null) return null;
|
||||||
|
return this.normalizeBearerToken(storage.getItem(this.bearerStorageKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequestHeaders(headers = {}) {
|
||||||
|
var requestHeaders = Object.assign({}, headers);
|
||||||
|
if (typeof requestHeaders.Authorization === 'undefined') {
|
||||||
|
var token = this.getBearerToken();
|
||||||
|
if (token != null) requestHeaders.Authorization = 'Bearer ' + token;
|
||||||
|
}
|
||||||
|
return requestHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyHeaders(xhttp, headers) {
|
||||||
|
Object.keys(headers).forEach(key => {
|
||||||
|
var value = headers[key];
|
||||||
|
if (typeof value === 'undefined' || value === null) return;
|
||||||
|
xhttp.setRequestHeader(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bearerSet(token) {
|
||||||
|
var storage = this.getStorage();
|
||||||
|
if (storage == null) return;
|
||||||
|
token = this.normalizeBearerToken(token);
|
||||||
|
if (token == null) {
|
||||||
|
storage.removeItem(this.bearerStorageKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storage.setItem(this.bearerStorageKey, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------
|
||||||
|
* General API call
|
||||||
|
*/
|
||||||
|
call(method, data, callback) {
|
||||||
|
var xhttp = new XMLHttpRequest();
|
||||||
|
var headers = this.getRequestHeaders();
|
||||||
|
xhttp.withCredentials = true;
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (this.readyState === 4) {
|
||||||
|
if (this.status === 200) {
|
||||||
|
if (callback != null) callback(JSON.parse(this.responseText));
|
||||||
|
} else {
|
||||||
|
if (callback != null) callback({'status': 'ERROR', 'msg': 'HTTP STATUS ' + this.status});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var form_data = new FormData();
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
let val = data[key];
|
||||||
|
if (typeof val === 'undefined') return;
|
||||||
|
if (typeof val == 'object') val = JSON.stringify(val);
|
||||||
|
form_data.append(key, val);
|
||||||
|
});
|
||||||
|
xhttp.open('POST', this.endpoint + '?action=' + method);
|
||||||
|
this.applyHeaders(xhttp, headers);
|
||||||
|
xhttp.send(form_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
callPromise(method, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.call(method, data, function(response) {
|
||||||
|
if (method === '__HELP__') {
|
||||||
|
resolve(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.status === 'OK') {
|
||||||
|
resolve(response.data);
|
||||||
|
} else {
|
||||||
|
reject(response.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------
|
||||||
|
* API actions
|
||||||
|
*/
|
||||||
|
help() {
|
||||||
|
return this.callPromise('__HELP__', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
<?php if (is_array($this->methods)) foreach ($this->methods as $method) {
|
||||||
|
echo "\t".$method['name'].'('.implode(', ', array_map(function($param) { return $param['name']; }, $method['params'])).') {';
|
||||||
|
echo "\n\t\treturn this.callPromise('".$method['name']."', {".implode(', ', array_map(function($param) { return $param['name'].': '.$param['name']; }, $method['params']))."});";
|
||||||
|
echo "\n\t}\n\n";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default new <?php echo $this->apiName; ?>();
|
||||||
@ -1,3 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
$mapType = function (?string $type): string {
|
||||||
|
if (!is_string($type) || $type === '') {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = strtolower(ltrim($type, '\\'));
|
||||||
|
|
||||||
|
return match ($type) {
|
||||||
|
'int', 'integer', 'float', 'double' => 'number',
|
||||||
|
'string' => 'string',
|
||||||
|
'bool', 'boolean' => 'boolean',
|
||||||
|
'array', 'iterable' => 'unknown[]',
|
||||||
|
'object', 'stdclass' => 'Record<string, unknown>',
|
||||||
|
'mixed', 'resource' => 'unknown',
|
||||||
|
'null', 'void' => 'null',
|
||||||
|
'scalar' => 'string | number | boolean',
|
||||||
|
'callable', 'closure' => '(...args: unknown[]) => unknown',
|
||||||
|
'never' => 'never',
|
||||||
|
default => 'unknown',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$mapUnionType = function (mixed $type) use ($mapType): string {
|
||||||
|
if (is_array($type)) {
|
||||||
|
$parts = array();
|
||||||
|
foreach ($type as $singleType) {
|
||||||
|
$parts[] = $mapType(is_string($singleType) ? $singleType : null);
|
||||||
|
}
|
||||||
|
$parts = array_values(array_unique($parts));
|
||||||
|
return empty($parts) ? 'unknown' : implode(' | ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mapType(is_string($type) ? $type : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
$normalizeTypeStructures = function (mixed $structure): array {
|
||||||
|
if (!is_array($structure)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
if (array_key_exists('type', $structure) && array_key_exists('properties', $structure)) {
|
||||||
|
return array($structure);
|
||||||
|
}
|
||||||
|
return array_values(array_filter($structure, 'is_array'));
|
||||||
|
};
|
||||||
|
|
||||||
|
$findTypeStructure = function (?string $type, mixed $structure) use ($normalizeTypeStructures): array|null {
|
||||||
|
if (!is_string($type) || $type === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$type = ltrim($type, '\\');
|
||||||
|
foreach ($normalizeTypeStructures($structure) as $one_structure) {
|
||||||
|
if (($one_structure['type'] ?? null) === $type) {
|
||||||
|
return $one_structure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$mapTypeWithStructure = null;
|
||||||
|
$mapUnionTypeWithStructure = null;
|
||||||
|
|
||||||
|
$mapTypeWithStructure = function (?string $type, mixed $structure) use ($mapType, &$mapUnionTypeWithStructure): string {
|
||||||
|
if (is_array($structure) && array_key_exists('type', $structure) && array_key_exists('properties', $structure)) {
|
||||||
|
$properties = array();
|
||||||
|
foreach (($structure['properties'] ?? array()) as $property) {
|
||||||
|
if (!is_array($property) || !isset($property['name'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$propertyType = $mapUnionTypeWithStructure($property['type'] ?? null, $property['type_structure'] ?? null);
|
||||||
|
if (!empty($property['nullable']) && strpos($propertyType, 'null') === false) {
|
||||||
|
$propertyType .= ' | null';
|
||||||
|
}
|
||||||
|
$properties[] = $property['name'] . ': ' . $propertyType;
|
||||||
|
}
|
||||||
|
return empty($properties) ? 'Record<string, unknown>' : '{ ' . implode('; ', $properties) . ' }';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mapType($type);
|
||||||
|
};
|
||||||
|
|
||||||
|
$mapUnionTypeWithStructure = function (mixed $type, mixed $structure) use ($mapType, $findTypeStructure, &$mapTypeWithStructure): string {
|
||||||
|
if (is_array($type)) {
|
||||||
|
$parts = array();
|
||||||
|
foreach ($type as $singleType) {
|
||||||
|
$typeName = is_string($singleType) ? $singleType : null;
|
||||||
|
$parts[] = $mapTypeWithStructure($typeName, $findTypeStructure($typeName, $structure));
|
||||||
|
}
|
||||||
|
$parts = array_values(array_unique($parts));
|
||||||
|
return empty($parts) ? 'unknown' : implode(' | ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mapTypeWithStructure(is_string($type) ? $type : null, $structure);
|
||||||
|
};
|
||||||
|
?>
|
||||||
/**
|
/**
|
||||||
* Generated by APIlite
|
* Generated by APIlite
|
||||||
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
||||||
@ -5,60 +100,201 @@
|
|||||||
* <?php echo date('Y-m-d H:i:s'); ?>
|
* <?php echo date('Y-m-d H:i:s'); ?>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class <?php echo $this->apiName; ?> {
|
export interface APIliteActionResponse<T> {
|
||||||
endpont = "<?php echo $this->endpoint; ?>";
|
status: 'OK';
|
||||||
|
data: T;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
export interface APIliteErrorResponse {
|
||||||
* General API call
|
status: 'ERROR';
|
||||||
*/
|
msg: string;
|
||||||
call(method, data, callback) {
|
}
|
||||||
var xhttp = new XMLHttpRequest();
|
|
||||||
xhttp.withCredentials = true;
|
export interface APIliteMethodParam {
|
||||||
xhttp.onreadystatechange = function() {
|
name: string;
|
||||||
if (this.readyState == 4) {
|
type: string | null;
|
||||||
if (this.status == 200) {
|
optional: boolean;
|
||||||
if (callback != null) callback(JSON.parse(this.responseText));
|
default: unknown;
|
||||||
} else {
|
doc: string | null;
|
||||||
if (callback != null) callback({'status': 'ERROR', 'message': 'HTTP STATUS ' + this.status});
|
}
|
||||||
}
|
|
||||||
|
export interface APIliteMethodDoc {
|
||||||
|
name: string;
|
||||||
|
doc: string | null;
|
||||||
|
description: string | null;
|
||||||
|
params: APIliteMethodParam[];
|
||||||
|
return: string | string[] | null;
|
||||||
|
return_structure: APIliteTypeStructure | APIliteTypeStructure[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIliteTypeStructureProperty {
|
||||||
|
name: string;
|
||||||
|
type: string | string[] | null;
|
||||||
|
nullable: boolean;
|
||||||
|
type_structure: APIliteTypeStructure | APIliteTypeStructure[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIliteTypeStructure {
|
||||||
|
type: string;
|
||||||
|
recursive?: boolean;
|
||||||
|
properties: APIliteTypeStructureProperty[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIliteHelpResponse {
|
||||||
|
name: string;
|
||||||
|
html_version: string;
|
||||||
|
javascript_version: string;
|
||||||
|
typescript_version: string;
|
||||||
|
actions: APIliteMethodDoc[];
|
||||||
|
status: string;
|
||||||
|
data: string;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIliteRequestHeaders = Partial<Record<string, string>>;
|
||||||
|
|
||||||
|
class <?php echo $this->apiName; ?> {
|
||||||
|
endpoint: string = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
|
||||||
|
private readonly bearerStorageKey: string = 'apilite_bearer_token';
|
||||||
|
|
||||||
|
private normalizeBearerToken(token: string | null | undefined): string | null {
|
||||||
|
if (typeof token !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalizedToken = token.trim();
|
||||||
|
return normalizedToken === '' ? null : normalizedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStorage(): Storage | null {
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
|
||||||
|
return window.localStorage;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBearerToken(): string | null {
|
||||||
|
const storage = this.getStorage();
|
||||||
|
if (storage === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.normalizeBearerToken(storage.getItem(this.bearerStorageKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRequestHeaders(headers: APIliteRequestHeaders = {}): APIliteRequestHeaders {
|
||||||
|
const requestHeaders: APIliteRequestHeaders = { ...headers };
|
||||||
|
if (typeof requestHeaders.Authorization === 'undefined') {
|
||||||
|
const token = this.getBearerToken();
|
||||||
|
if (token !== null) {
|
||||||
|
requestHeaders.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var form_data = new FormData();
|
return requestHeaders;
|
||||||
Object.keys(data).forEach(key => {
|
}
|
||||||
let val = data[key];
|
|
||||||
if (typeof val == 'object') val = JSON.stringify(val);
|
private applyHeaders(xhttp: XMLHttpRequest, headers: APIliteRequestHeaders): void {
|
||||||
form_data.append(key, val);
|
Object.keys(headers).forEach((key) => {
|
||||||
|
const value = headers[key];
|
||||||
|
if (typeof value === 'undefined' || value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xhttp.setRequestHeader(key, value);
|
||||||
});
|
});
|
||||||
xhttp.open('POST', this.endpont + '?action=' + method);
|
|
||||||
xhttp.send(form_data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
callPromise(method, data) {
|
bearerSet(token: string | null): void {
|
||||||
return new Promise((resolve, reject) => {
|
const storage = this.getStorage();
|
||||||
this.call(method, data, function(response) {
|
if (storage === null) {
|
||||||
if (response.status == 'OK') {
|
return;
|
||||||
resolve(response.data);
|
}
|
||||||
|
const normalizedToken = this.normalizeBearerToken(token);
|
||||||
|
if (normalizedToken === null) {
|
||||||
|
storage.removeItem(this.bearerStorageKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storage.setItem(this.bearerStorageKey, normalizedToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private call(
|
||||||
|
method: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
callback: (response: APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse) => void
|
||||||
|
): void {
|
||||||
|
const xhttp = new XMLHttpRequest();
|
||||||
|
const headers = this.getRequestHeaders();
|
||||||
|
xhttp.withCredentials = true;
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (this.readyState === 4) {
|
||||||
|
if (this.status === 200) {
|
||||||
|
const response = JSON.parse(this.responseText) as APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse;
|
||||||
|
callback(response);
|
||||||
} else {
|
} else {
|
||||||
reject(response.msg);
|
callback({ status: 'ERROR', msg: 'HTTP STATUS ' + this.status });
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
})
|
};
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
const rawValue = data[key];
|
||||||
|
if (typeof rawValue === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let value: string | Blob;
|
||||||
|
if (rawValue instanceof Blob) {
|
||||||
|
value = rawValue;
|
||||||
|
} else if (typeof rawValue === 'object' && rawValue !== null) {
|
||||||
|
value = JSON.stringify(rawValue);
|
||||||
|
} else {
|
||||||
|
value = String(rawValue);
|
||||||
|
}
|
||||||
|
formData.append(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
xhttp.open('POST', this.endpoint + '?action=' + method);
|
||||||
|
this.applyHeaders(xhttp, headers);
|
||||||
|
xhttp.send(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
private callPromise<T>(method: string, data: Record<string, unknown>): Promise<T> {
|
||||||
* API actions
|
return new Promise<T>((resolve, reject) => {
|
||||||
*/
|
this.call(method, data, (response) => {
|
||||||
help() {
|
if (method === '__HELP__') {
|
||||||
return this.callPromise('__HELP__', {});
|
resolve(response as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.status === 'OK') {
|
||||||
|
resolve(response.data as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(response.msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
help(): Promise<APIliteHelpResponse> {
|
||||||
|
return this.callPromise<APIliteHelpResponse>('__HELP__', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
<?php if (is_array($this->methods)) foreach ($this->methods as $method) {
|
<?php if (is_array($this->methods)) foreach ($this->methods as $method) {
|
||||||
echo "\t".$method['name'].'('.implode(', ', array_map(function($param) { return $param['name']; }, $method['params'])).') {';
|
$paramsSignature = array();
|
||||||
echo "\n\t\treturn this.callPromise('".$method['name']."', {".implode(', ', array_map(function($param) { return $param['name'].': '.$param['name']; }, $method['params']))."});";
|
$paramsPayload = array();
|
||||||
echo "\n\t}\n\n";
|
foreach ($method['params'] as $param) {
|
||||||
|
$paramType = $mapType($param['type']);
|
||||||
|
$paramsSignature[] = $param['name'] . ($param['optional'] ? '?' : '') . ': ' . $paramType;
|
||||||
|
$paramsPayload[] = $param['name'];
|
||||||
}
|
}
|
||||||
|
$returnType = $mapUnionTypeWithStructure($method['return'], $method['return_structure'] ?? null);
|
||||||
?>
|
?>
|
||||||
|
<?php echo $method['name']; ?>(<?php echo implode(', ', $paramsSignature); ?>): Promise<APIliteActionResponse<<?php echo $returnType; ?>>> {
|
||||||
|
return this.callPromise<APIliteActionResponse<<?php echo $returnType; ?>>>('<?php echo $method['name']; ?>', { <?php echo implode(', ', $paramsPayload); ?> });
|
||||||
|
}
|
||||||
|
|
||||||
};
|
<?php } ?>
|
||||||
|
}
|
||||||
|
|
||||||
export default new BackendAPI();
|
export default new <?php echo $this->apiName; ?>();
|
||||||
Reference in New Issue
Block a user