added into generated client response handlers
This commit is contained in:
@ -23,6 +23,7 @@ The HTML help page is also interactive:
|
|||||||
- JavaScript stays plain JS.
|
- JavaScript stays plain JS.
|
||||||
- TypeScript stays typed (interfaces + typed method signatures).
|
- 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).
|
- Bearer token behavior stays aligned between JavaScript and TypeScript clients (`bearerSet()`, `apilite_bearer_token`, automatic `Authorization: Bearer ...` header).
|
||||||
|
- Response/error handler behavior stays aligned between JavaScript and TypeScript clients (`addResponseHandler()`, `addErrorHandler()`, unsubscribe return value, `(response, context)` arguments, ignored handler exceptions).
|
||||||
- Keep the HTML help page usable both as documentation and as a lightweight in-browser tester.
|
- 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 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.
|
- If you change request metadata or tester behavior, update `README.md` and keep `src/help.tpl.php` aligned with the actual runtime request model.
|
||||||
@ -44,3 +45,4 @@ The HTML help page is also interactive:
|
|||||||
## Documentation rule
|
## Documentation rule
|
||||||
When output format names/flags/URLs change, update `README.md` in the same change set.
|
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.
|
When Bearer token helper/storage behavior changes, update `README.md` and keep JS/TS client docs aligned with the actual generated templates.
|
||||||
|
When generated client handler behavior changes, update `README.md` and keep JS/TS client docs aligned with the actual generated templates.
|
||||||
|
|||||||
18
README.md
18
README.md
@ -240,16 +240,29 @@ You can also download generated frontend clients:
|
|||||||
|
|
||||||
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 ...`.
|
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 ...`.
|
||||||
|
|
||||||
|
Both generated clients also support response hooks:
|
||||||
|
|
||||||
|
* `addResponseHandler(handler)` registers a handler called for every API response.
|
||||||
|
* `addErrorHandler(handler)` registers a handler called only when the API response has `status === 'ERROR'`.
|
||||||
|
* Both methods return an unsubscribe function.
|
||||||
|
* Handlers receive `(response, context)`, where `context` contains `method`, `data`, and `httpStatus`.
|
||||||
|
* Handler exceptions are ignored, so they do not change the original API callback or promise result.
|
||||||
|
|
||||||
JavaScript usage example:
|
JavaScript usage example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import backend from './backend.js';
|
import backend from './backend.js';
|
||||||
|
|
||||||
backend.bearerSet('your-access-token');
|
backend.bearerSet('your-access-token');
|
||||||
|
const removeErrorHandler = backend.addErrorHandler((response, context) => {
|
||||||
|
console.error(context.method, response.msg);
|
||||||
|
});
|
||||||
|
|
||||||
backend.add(1, 2).then((response) => {
|
backend.add(1, 2).then((response) => {
|
||||||
console.log(response.data);
|
console.log(response.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// removeErrorHandler();
|
||||||
```
|
```
|
||||||
|
|
||||||
TypeScript usage example (Vue + TS):
|
TypeScript usage example (Vue + TS):
|
||||||
@ -258,10 +271,15 @@ TypeScript usage example (Vue + TS):
|
|||||||
import backend from './backend';
|
import backend from './backend';
|
||||||
|
|
||||||
backend.bearerSet('your-access-token');
|
backend.bearerSet('your-access-token');
|
||||||
|
const removeResponseHandler = backend.addResponseHandler((response, context) => {
|
||||||
|
console.log(context.httpStatus, response.status);
|
||||||
|
});
|
||||||
|
|
||||||
backend.add(1, 2).then((response) => {
|
backend.add(1, 2).then((response) => {
|
||||||
console.log(response.data); // typed value based on PHP return type
|
console.log(response.data); // typed value based on PHP return type
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// removeResponseHandler();
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|||||||
@ -8,6 +8,8 @@
|
|||||||
class <?php echo $this->apiName; ?> {
|
class <?php echo $this->apiName; ?> {
|
||||||
endpoint = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
|
endpoint = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
|
||||||
bearerStorageKey = 'apilite_bearer_token';
|
bearerStorageKey = 'apilite_bearer_token';
|
||||||
|
responseHandlers = [];
|
||||||
|
errorHandlers = [];
|
||||||
|
|
||||||
normalizeBearerToken(token) {
|
normalizeBearerToken(token) {
|
||||||
if (typeof token !== 'string') return null;
|
if (typeof token !== 'string') return null;
|
||||||
@ -58,20 +60,64 @@ class <?php echo $this->apiName; ?> {
|
|||||||
storage.setItem(this.bearerStorageKey, token);
|
storage.setItem(this.bearerStorageKey, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addResponseHandler(handler) {
|
||||||
|
this.responseHandlers.push(handler);
|
||||||
|
return () => {
|
||||||
|
this.responseHandlers = this.responseHandlers.filter(registeredHandler => registeredHandler !== handler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
addErrorHandler(handler) {
|
||||||
|
this.errorHandlers.push(handler);
|
||||||
|
return () => {
|
||||||
|
this.errorHandlers = this.errorHandlers.filter(registeredHandler => registeredHandler !== handler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
emitResponseHandlers(response, context) {
|
||||||
|
this.responseHandlers.forEach(handler => {
|
||||||
|
try {
|
||||||
|
handler(response, context);
|
||||||
|
} catch (error) {
|
||||||
|
// Response handlers must not change the API request result.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emitErrorHandlers(response, context) {
|
||||||
|
this.errorHandlers.forEach(handler => {
|
||||||
|
try {
|
||||||
|
handler(response, context);
|
||||||
|
} catch (error) {
|
||||||
|
// Error handlers must not change the API request result.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emitHandlers(response, context) {
|
||||||
|
this.emitResponseHandlers(response, context);
|
||||||
|
if (response.status === 'ERROR') this.emitErrorHandlers(response, context);
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
* General API call
|
* General API call
|
||||||
*/
|
*/
|
||||||
call(method, data, callback) {
|
call(method, data, callback) {
|
||||||
var xhttp = new XMLHttpRequest();
|
var xhttp = new XMLHttpRequest();
|
||||||
var headers = this.getRequestHeaders();
|
var headers = this.getRequestHeaders();
|
||||||
|
var api = this;
|
||||||
xhttp.withCredentials = true;
|
xhttp.withCredentials = true;
|
||||||
xhttp.onreadystatechange = function() {
|
xhttp.onreadystatechange = function() {
|
||||||
if (this.readyState === 4) {
|
if (this.readyState === 4) {
|
||||||
|
var response;
|
||||||
if (this.status === 200) {
|
if (this.status === 200) {
|
||||||
if (callback != null) callback(JSON.parse(this.responseText));
|
response = JSON.parse(this.responseText);
|
||||||
} else {
|
} else {
|
||||||
if (callback != null) callback({'status': 'ERROR', 'msg': 'HTTP STATUS ' + this.status});
|
response = {'status': 'ERROR', 'msg': 'HTTP STATUS ' + this.status};
|
||||||
}
|
}
|
||||||
|
var context = {method: method, data: data, httpStatus: this.status};
|
||||||
|
api.emitHandlers(response, context);
|
||||||
|
if (callback != null) callback(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var form_data = new FormData();
|
var form_data = new FormData();
|
||||||
@ -118,4 +164,4 @@ class <?php echo $this->apiName; ?> {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default new <?php echo $this->apiName; ?>();
|
export default new <?php echo $this->apiName; ?>();
|
||||||
|
|||||||
@ -152,11 +152,23 @@ export interface APIliteHelpResponse {
|
|||||||
msg: string;
|
msg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type APIliteResponse = APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse;
|
||||||
|
|
||||||
|
export interface APIliteResponseContext {
|
||||||
|
method: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
httpStatus: number;
|
||||||
|
}
|
||||||
|
|
||||||
type APIliteRequestHeaders = Partial<Record<string, string>>;
|
type APIliteRequestHeaders = Partial<Record<string, string>>;
|
||||||
|
type APIliteResponseHandler = (response: APIliteResponse, context: APIliteResponseContext) => void;
|
||||||
|
type APIliteErrorHandler = (response: APIliteErrorResponse, context: APIliteResponseContext) => void;
|
||||||
|
|
||||||
class <?php echo $this->apiName; ?> {
|
class <?php echo $this->apiName; ?> {
|
||||||
endpoint: string = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
|
endpoint: string = <?php echo sprintf(substr($this->endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>;
|
||||||
private readonly bearerStorageKey: string = 'apilite_bearer_token';
|
private readonly bearerStorageKey: string = 'apilite_bearer_token';
|
||||||
|
private responseHandlers: APIliteResponseHandler[] = [];
|
||||||
|
private errorHandlers: APIliteErrorHandler[] = [];
|
||||||
|
|
||||||
private normalizeBearerToken(token: string | null | undefined): string | null {
|
private normalizeBearerToken(token: string | null | undefined): string | null {
|
||||||
if (typeof token !== 'string') {
|
if (typeof token !== 'string') {
|
||||||
@ -219,22 +231,71 @@ class <?php echo $this->apiName; ?> {
|
|||||||
storage.setItem(this.bearerStorageKey, normalizedToken);
|
storage.setItem(this.bearerStorageKey, normalizedToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addResponseHandler(handler: APIliteResponseHandler): () => void {
|
||||||
|
this.responseHandlers.push(handler);
|
||||||
|
return () => {
|
||||||
|
this.responseHandlers = this.responseHandlers.filter((registeredHandler) => registeredHandler !== handler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
addErrorHandler(handler: APIliteErrorHandler): () => void {
|
||||||
|
this.errorHandlers.push(handler);
|
||||||
|
return () => {
|
||||||
|
this.errorHandlers = this.errorHandlers.filter((registeredHandler) => registeredHandler !== handler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitResponseHandlers(response: APIliteResponse, context: APIliteResponseContext): void {
|
||||||
|
this.responseHandlers.forEach((handler) => {
|
||||||
|
try {
|
||||||
|
handler(response, context);
|
||||||
|
} catch {
|
||||||
|
// Response handlers must not change the API request result.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitErrorHandlers(response: APIliteErrorResponse, context: APIliteResponseContext): void {
|
||||||
|
this.errorHandlers.forEach((handler) => {
|
||||||
|
try {
|
||||||
|
handler(response, context);
|
||||||
|
} catch {
|
||||||
|
// Error handlers must not change the API request result.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private isErrorResponse(response: APIliteResponse): response is APIliteErrorResponse {
|
||||||
|
return response.status === 'ERROR';
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitHandlers(response: APIliteResponse, context: APIliteResponseContext): void {
|
||||||
|
this.emitResponseHandlers(response, context);
|
||||||
|
if (this.isErrorResponse(response)) {
|
||||||
|
this.emitErrorHandlers(response, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private call(
|
private call(
|
||||||
method: string,
|
method: string,
|
||||||
data: Record<string, unknown>,
|
data: Record<string, unknown>,
|
||||||
callback: (response: APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse) => void
|
callback: (response: APIliteResponse) => void
|
||||||
): void {
|
): void {
|
||||||
const xhttp = new XMLHttpRequest();
|
const xhttp = new XMLHttpRequest();
|
||||||
const headers = this.getRequestHeaders();
|
const headers = this.getRequestHeaders();
|
||||||
|
const api = this;
|
||||||
xhttp.withCredentials = true;
|
xhttp.withCredentials = true;
|
||||||
xhttp.onreadystatechange = function() {
|
xhttp.onreadystatechange = function() {
|
||||||
if (this.readyState === 4) {
|
if (this.readyState === 4) {
|
||||||
|
let response: APIliteResponse;
|
||||||
if (this.status === 200) {
|
if (this.status === 200) {
|
||||||
const response = JSON.parse(this.responseText) as APIliteHelpResponse | APIliteActionResponse<unknown> | APIliteErrorResponse;
|
response = JSON.parse(this.responseText) as APIliteResponse;
|
||||||
callback(response);
|
|
||||||
} else {
|
} else {
|
||||||
callback({ status: 'ERROR', msg: 'HTTP STATUS ' + this.status });
|
response = { status: 'ERROR', msg: 'HTTP STATUS ' + this.status };
|
||||||
}
|
}
|
||||||
|
const context: APIliteResponseContext = { method, data, httpStatus: this.status };
|
||||||
|
api.emitHandlers(response, context);
|
||||||
|
callback(response);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -297,4 +358,4 @@ class <?php echo $this->apiName; ?> {
|
|||||||
<?php } ?>
|
<?php } ?>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new <?php echo $this->apiName; ?>();
|
export default new <?php echo $this->apiName; ?>();
|
||||||
|
|||||||
Reference in New Issue
Block a user