diff --git a/README.md b/README.md index 6c55592..d0f6b4b 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,28 @@ The stackable uses a `platformatic.json` configuration file: "$schema": "https://schemas.platformatic.dev/@platformatic/php/0.4.3.json", "module": "@platformatic/php", "php": { - "docroot": "public" + "docroot": "public", + "rewriter": [ + { + "operation": "and", + "conditions": [ + { + "type": "path", + "args": ["^/api/.*"] + }, + { + "type": "method", + "args": ["POST"] + } + ], + "rewriters": [ + { + "type": "path", + "args": ["api.php"] + } + ] + } + ] }, "server": { "hostname": "{PLT_SERVER_HOSTNAME}", @@ -64,6 +85,17 @@ The stackable uses a `platformatic.json` configuration file: #### php - `docroot` (string, required) - Path to the root directory containing PHP files +- `rewriter` (array, optional) - A sequence of conditional rewrites to apply to PHP requests + - Each conditional rewrite is an object with: + - `operation` (string, optional) - Either `and` or `or` to combine conditions, defaults to `and`. + - `conditions` (array, optional) - List of conditions to match, if any + - Each condition is an object with: + - `type` (string, required) - Type of condition (e.g., `path`, `method`) + - `args` (array, required) - Parameters for the condition + - `rewriters` (array, required) - List of rewriters to apply if conditions match + - Each rewriter is an object with: + - `type` (string, required) - Type of rewriter (e.g., `path`, `method`) + - `args` (array, required) - Parameters for the rewriter #### server Standard Platformatic server configuration options are supported. @@ -150,7 +182,7 @@ echo json_encode([ // public/api.php if ($_SERVER['REQUEST_METHOD'] === 'POST') { $input = json_decode(file_get_contents('php://input'), true); - + header("Content-Type: application/json"); echo json_encode([ "received" => $input, diff --git a/config.d.ts b/config.d.ts index 16f26a6..c0fcdcc 100644 --- a/config.d.ts +++ b/config.d.ts @@ -72,6 +72,13 @@ export interface PlatformaticPHPConfiguration { paths: string[]; censor?: string; }; + base?: { + [k: string]: unknown; + } | null; + messageKey?: string; + customLevels?: { + [k: string]: unknown; + }; [k: string]: unknown; }; loggerInstance?: { @@ -175,7 +182,82 @@ export interface PlatformaticPHPConfiguration { [k: string]: string; }; }; - telemetry?: OpenTelemetry; + telemetry?: { + enabled?: boolean | string; + /** + * The name of the service. Defaults to the folder name if not specified. + */ + serviceName: string; + /** + * The version of the service (optional) + */ + version?: string; + /** + * An array of paths to skip when creating spans. Useful for health checks and other endpoints that do not need to be traced. + */ + skip?: { + /** + * The path to skip. Can be a string or a regex. + */ + path?: string; + /** + * HTTP method to skip + */ + method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"; + [k: string]: unknown; + }[]; + exporter?: + | { + type?: "console" | "otlp" | "zipkin" | "memory" | "file"; + /** + * Options for the exporter. These are passed directly to the exporter. + */ + options?: { + /** + * The URL to send the traces to. Not used for console or memory exporters. + */ + url?: string; + /** + * Headers to send to the exporter. Not used for console or memory exporters. + */ + headers?: { + [k: string]: unknown; + }; + /** + * The path to write the traces to. Only for file exporter. + */ + path?: string; + [k: string]: unknown; + }; + additionalProperties?: never; + [k: string]: unknown; + }[] + | { + type?: "console" | "otlp" | "zipkin" | "memory" | "file"; + /** + * Options for the exporter. These are passed directly to the exporter. + */ + options?: { + /** + * The URL to send the traces to. Not used for console or memory exporters. + */ + url?: string; + /** + * Headers to send to the exporter. Not used for console or memory exporters. + */ + headers?: { + [k: string]: unknown; + }; + /** + * The path to write the traces to. Only for file exporter. + */ + path?: string; + [k: string]: unknown; + }; + additionalProperties?: never; + [k: string]: unknown; + }; + }; watch?: | { enabled?: boolean | string; @@ -253,89 +335,360 @@ export interface PlatformaticPHPConfiguration { fullRequest?: boolean; validateResponse?: boolean; }[]; - php?: { - /** - * Path to the root of the PHP project - */ - docroot: string; - [k: string]: unknown; - }; -} -export interface OpenTelemetry { - enabled?: boolean | string; - /** - * The name of the service. Defaults to the folder name if not specified. - */ - serviceName: string; - /** - * The version of the service (optional) - */ - version?: string; - /** - * An array of paths to skip when creating spans. Useful for health checks and other endpoints that do not need to be traced. - */ - skip?: { - /** - * The path to skip. Can be a string or a regex. - */ - path?: string; - /** - * HTTP method to skip - */ - method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"; - [k: string]: unknown; - }[]; - exporter?: - | { - type?: "console" | "otlp" | "zipkin" | "memory" | "file"; - /** - * Options for the exporter. These are passed directly to the exporter. - */ - options?: { - /** - * The URL to send the traces to. Not used for console or memory exporters. - */ - url?: string; - /** - * Headers to send to the exporter. Not used for console or memory exporters. - */ - headers?: { + runtime?: { + preload?: string | string[]; + basePath?: string; + workers?: number | string; + logger?: { + level: ( + | ("fatal" | "error" | "warn" | "info" | "debug" | "trace" | "silent") + | { [k: string]: unknown; + } + ) & + string; + transport?: + | { + target?: string; + options?: { + [k: string]: unknown; + }; + } + | { + targets?: { + target?: string; + options?: { + [k: string]: unknown; + }; + level?: string; + }[]; + options?: { + [k: string]: unknown; + }; }; - /** - * The path to write the traces to. Only for file exporter. - */ - path?: string; + pipeline?: { + target?: string; + options?: { [k: string]: unknown; }; - additionalProperties?: never; + }; + formatters?: { + path: string; + }; + timestamp?: "epochTime" | "unixTime" | "nullTime" | "isoTime"; + redact?: { + paths: string[]; + censor?: string; + }; + base?: { [k: string]: unknown; - }[] - | { - type?: "console" | "otlp" | "zipkin" | "memory" | "file"; - /** - * Options for the exporter. These are passed directly to the exporter. - */ - options?: { - /** - * The URL to send the traces to. Not used for console or memory exporters. - */ - url?: string; - /** - * Headers to send to the exporter. Not used for console or memory exporters. - */ - headers?: { + } | null; + messageKey?: string; + customLevels?: { + [k: string]: unknown; + }; + [k: string]: unknown; + }; + server?: { + hostname?: string; + port?: number | string; + http2?: boolean; + https?: { + allowHTTP1?: boolean; + key: + | string + | { + path?: string; + } + | ( + | string + | { + path?: string; + } + )[]; + cert: + | string + | { + path?: string; + } + | ( + | string + | { + path?: string; + } + )[]; + requestCert?: boolean; + rejectUnauthorized?: boolean; + }; + }; + startTimeout?: number; + restartOnError?: boolean | number; + gracefulShutdown?: { + runtime: number | string; + service: number | string; + }; + health?: { + enabled?: boolean | string; + interval?: number | string; + gracePeriod?: number | string; + maxUnhealthyChecks?: number | string; + maxELU?: number | string; + maxHeapUsed?: number | string; + maxHeapTotal?: number | string; + maxYoungGeneration?: number; + }; + undici?: { + agentOptions?: { + [k: string]: unknown; + }; + interceptors?: + | { + module: string; + options: { + [k: string]: unknown; + }; + [k: string]: unknown; + }[] + | { + Client?: { + module: string; + options: { + [k: string]: unknown; + }; + [k: string]: unknown; + }[]; + Pool?: { + module: string; + options: { + [k: string]: unknown; + }; + [k: string]: unknown; + }[]; + Agent?: { + module: string; + options: { + [k: string]: unknown; + }; + [k: string]: unknown; + }[]; [k: string]: unknown; }; + [k: string]: unknown; + }; + httpCache?: + | boolean + | { + store?: string; /** - * The path to write the traces to. Only for file exporter. + * @minItems 1 */ - path?: string; + methods?: [string, ...string[]]; + cacheTagsHeader?: string; + maxSize?: number; + maxEntrySize?: number; + maxCount?: number; [k: string]: unknown; }; - additionalProperties?: never; + watch?: boolean | string; + managementApi?: + | boolean + | string + | { + logs?: { + maxSize?: number; + }; + }; + metrics?: + | boolean + | { + port?: number | string; + enabled?: boolean | string; + hostname?: string; + endpoint?: string; + auth?: { + username: string; + password: string; + }; + labels?: { + [k: string]: string; + }; + readiness?: + | boolean + | { + endpoint?: string; + success?: { + statusCode?: number; + body?: string; + }; + fail?: { + statusCode?: number; + body?: string; + }; + }; + liveness?: + | boolean + | { + endpoint?: string; + success?: { + statusCode?: number; + body?: string; + }; + fail?: { + statusCode?: number; + body?: string; + }; + }; + additionalProperties?: never; + [k: string]: unknown; + }; + telemetry?: { + enabled?: boolean | string; + /** + * The name of the service. Defaults to the folder name if not specified. + */ + serviceName: string; + /** + * The version of the service (optional) + */ + version?: string; + /** + * An array of paths to skip when creating spans. Useful for health checks and other endpoints that do not need to be traced. + */ + skip?: { + /** + * The path to skip. Can be a string or a regex. + */ + path?: string; + /** + * HTTP method to skip + */ + method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"; [k: string]: unknown; + }[]; + exporter?: + | { + type?: "console" | "otlp" | "zipkin" | "memory" | "file"; + /** + * Options for the exporter. These are passed directly to the exporter. + */ + options?: { + /** + * The URL to send the traces to. Not used for console or memory exporters. + */ + url?: string; + /** + * Headers to send to the exporter. Not used for console or memory exporters. + */ + headers?: { + [k: string]: unknown; + }; + /** + * The path to write the traces to. Only for file exporter. + */ + path?: string; + [k: string]: unknown; + }; + additionalProperties?: never; + [k: string]: unknown; + }[] + | { + type?: "console" | "otlp" | "zipkin" | "memory" | "file"; + /** + * Options for the exporter. These are passed directly to the exporter. + */ + options?: { + /** + * The URL to send the traces to. Not used for console or memory exporters. + */ + url?: string; + /** + * Headers to send to the exporter. Not used for console or memory exporters. + */ + headers?: { + [k: string]: unknown; + }; + /** + * The path to write the traces to. Only for file exporter. + */ + path?: string; + [k: string]: unknown; + }; + additionalProperties?: never; + [k: string]: unknown; + }; + }; + inspectorOptions?: { + host?: string; + port?: number; + breakFirstLine?: boolean; + watchDisabled?: boolean; + [k: string]: unknown; + }; + serviceTimeout?: number | string; + env?: { + [k: string]: string; + }; + sourceMaps?: boolean; + scheduler?: { + enabled?: boolean | string; + name: string; + cron: string; + callbackUrl: string; + method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + headers?: { + [k: string]: string; }; + body?: + | string + | { + [k: string]: unknown; + }; + maxRetries?: number; + [k: string]: unknown; + }[]; + }; + php?: { + /** + * Path to the root of the PHP project + */ + docroot: string; + /** + * A sequence of conditional rewrites to apply to PHP requests + */ + rewriter?: { + /** + * Logical operation with which to group conditions + */ + operation?: "and" | "or"; + /** + * Conditions to match before applying rewrites + */ + conditions?: { + type?: string; + args?: string[]; + [k: string]: unknown; + }[]; + /** + * Set of rewrites to apply if conditions are met + * + * @minItems 1 + */ + rewriters: [ + { + type?: string; + args?: string[]; + [k: string]: unknown; + }, + ...{ + type?: string; + args?: string[]; + [k: string]: unknown; + }[] + ]; + [k: string]: unknown; + }[]; + [k: string]: unknown; + }; } export interface Info { title: string; diff --git a/lib/plugin.js b/lib/plugin.js index ebb5cf8..ed1f479 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -8,11 +8,12 @@ const capitalizeHeaders = header => header.replace(/(^|-)([a-z])/g, (_, dash, le export async function plugin (server, opts) { // We import this dynically to provide better error reporting in case // this module fails to load one of the native bindings - const { Php, Request } = await import('@platformatic/php-node') + const { Php, Request, Rewriter } = await import('@platformatic/php-node') /* c8 ignore next */ const configuration = server.platformatic?.config ?? opts.context?.stackable.configManager.current const docroot = configuration.php.docroot + let rewriter = configuration.php.rewriter // All files in the docroot that are not PHP files, should be served as static files await server.register(import('@fastify/static'), { @@ -28,9 +29,15 @@ export async function plugin (server, opts) { done(null, body) }) + // TODO: Make php-node support passing the rewriter config as an object to new Php() + if (rewriter) { + rewriter = new Rewriter(rewriter) + } + const php = new Php({ argv: process.argv, - docroot + docroot, + rewriter }) for (const method of HTTP_METHODS) { diff --git a/lib/schema.js b/lib/schema.js index d95066a..1355ef2 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -18,6 +18,46 @@ export const schema = { type: 'string', description: 'Path to the root of the PHP project', resolvePath: true + }, + rewriter: { + type: 'array', + description: 'A sequence of conditional rewrites to apply to PHP requests', + items: { + type: 'object', + description: 'A single conditional rewrite to apply to PHP requests', + properties: { + operation: { + type: 'string', + enum: ['and', 'or'], + description: 'Logical operation with which to group conditions' + }, + conditions: { + type: 'array', + description: 'Conditions to match before applying rewrites', + items: { + type: 'object', + properties: { + type: { type: 'string' }, + args: { type: 'array', items: { type: 'string' } } + } + } + }, + rewriters: { + type: 'array', + description: 'Set of rewrites to apply if conditions are met', + minItems: 1, + items: { + type: 'object', + properties: { + type: { type: 'string' }, + args: { type: 'array', items: { type: 'string' } } + } + } + } + }, + // If rewriter config is provided, it must have at least one rewriter + required: ['rewriters'], + } } }, required: ['docroot'], diff --git a/package.json b/package.json index 3cabf27..f344316 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "homepage": "https://github.com/platformatic/php#readme", "dependencies": { "@fastify/static": "^8.2.0", - "@platformatic/php-node": "^1.0.4", + "@platformatic/php-node": "^1.2.0", "@platformatic/service": "^2.63.3", "json-schema-to-typescript": "^15.0.4" }, diff --git a/schema.json b/schema.json index 895be89..cc7a8ab 100644 --- a/schema.json +++ b/schema.json @@ -1,7 +1,7 @@ { - "$id": "https://schemas.platformatic.dev/@platformatic/php/0.2.0.json", + "$id": "https://schemas.platformatic.dev/@platformatic/php/0.6.0.json", "title": "Platformatic PHP configuration", - "version": "0.2.0", + "version": "0.6.0", "type": "object", "properties": { "basePath": { @@ -220,6 +220,24 @@ "paths" ], "additionalProperties": false + }, + "base": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + }, + "messageKey": { + "type": "string" + }, + "customLevels": { + "type": "object", + "additionalProperties": true } }, "required": [ @@ -738,7 +756,6 @@ ] }, "telemetry": { - "$id": "/OpenTelemetry", "type": "object", "properties": { "enabled": { @@ -1121,13 +1138,1071 @@ "additionalProperties": false } }, - "php": { + "runtime": { "type": "object", "properties": { - "docroot": { - "type": "string", - "description": "Path to the root of the PHP project", - "resolvePath": true + "preload": { + "anyOf": [ + { + "type": "string", + "resolvePath": true + }, + { + "type": "array", + "items": { + "type": "string", + "resolvePath": true + } + } + ] + }, + "basePath": { + "type": "string" + }, + "workers": { + "anyOf": [ + { + "type": "number", + "minimum": 1 + }, + { + "type": "string" + } + ], + "default": 1 + }, + "logger": { + "type": "object", + "properties": { + "level": { + "type": "string", + "default": "info", + "oneOf": [ + { + "enum": [ + "fatal", + "error", + "warn", + "info", + "debug", + "trace", + "silent" + ] + }, + { + "pattern": "^\\{.+\\}$" + } + ] + }, + "transport": { + "anyOf": [ + { + "type": "object", + "properties": { + "target": { + "type": "string", + "resolveModule": true + }, + "options": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "targets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "target": { + "anyOf": [ + { + "type": "string", + "resolveModule": true + }, + { + "type": "string", + "resolvePath": true + } + ] + }, + "options": { + "type": "object" + }, + "level": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "options": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "pipeline": { + "type": "object", + "properties": { + "target": { + "type": "string", + "resolveModule": true + }, + "options": { + "type": "object" + } + }, + "additionalProperties": false + }, + "formatters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "resolvePath": true + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + "timestamp": { + "enum": [ + "epochTime", + "unixTime", + "nullTime", + "isoTime" + ] + }, + "redact": { + "type": "object", + "properties": { + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "censor": { + "type": "string", + "default": "[redacted]" + } + }, + "required": [ + "paths" + ], + "additionalProperties": false + }, + "base": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + }, + "messageKey": { + "type": "string" + }, + "customLevels": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "level" + ], + "default": {}, + "additionalProperties": true + }, + "server": { + "type": "object", + "properties": { + "hostname": { + "type": "string", + "default": "127.0.0.1" + }, + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "http2": { + "type": "boolean" + }, + "https": { + "type": "object", + "properties": { + "allowHTTP1": { + "type": "boolean" + }, + "key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string", + "resolvePath": true + } + }, + "additionalProperties": false + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string", + "resolvePath": true + } + }, + "additionalProperties": false + } + ] + } + } + ] + }, + "cert": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string", + "resolvePath": true + } + }, + "additionalProperties": false + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string", + "resolvePath": true + } + }, + "additionalProperties": false + } + ] + } + } + ] + }, + "requestCert": { + "type": "boolean" + }, + "rejectUnauthorized": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "key", + "cert" + ] + } + }, + "additionalProperties": false + }, + "startTimeout": { + "default": 30000, + "type": "number", + "minimum": 0 + }, + "restartOnError": { + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "number", + "minimum": 0 + } + ] + }, + "gracefulShutdown": { + "type": "object", + "properties": { + "runtime": { + "anyOf": [ + { + "type": "number", + "minimum": 1 + }, + { + "type": "string" + } + ], + "default": 10000 + }, + "service": { + "anyOf": [ + { + "type": "number", + "minimum": 1 + }, + { + "type": "string" + } + ], + "default": 10000 + } + }, + "default": {}, + "required": [ + "runtime", + "service" + ], + "additionalProperties": false + }, + "health": { + "type": "object", + "default": {}, + "properties": { + "enabled": { + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "interval": { + "default": 30000, + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "string" + } + ] + }, + "gracePeriod": { + "default": 30000, + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "string" + } + ] + }, + "maxUnhealthyChecks": { + "default": 10, + "anyOf": [ + { + "type": "number", + "minimum": 1 + }, + { + "type": "string" + } + ] + }, + "maxELU": { + "default": 0.99, + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "string" + } + ] + }, + "maxHeapUsed": { + "default": 0.99, + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "string" + } + ] + }, + "maxHeapTotal": { + "default": 4294967296, + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "string" + } + ] + }, + "maxYoungGeneration": { + "type": "number", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "undici": { + "type": "object", + "properties": { + "agentOptions": { + "type": "object", + "additionalProperties": true + }, + "interceptors": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "options": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "module", + "options" + ] + } + }, + { + "type": "object", + "properties": { + "Client": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "options": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "module", + "options" + ] + } + }, + "Pool": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "options": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "module", + "options" + ] + } + }, + "Agent": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "options": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "module", + "options" + ] + } + } + } + } + ] + } + } + }, + "httpCache": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "store": { + "type": "string" + }, + "methods": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "GET", + "HEAD" + ], + "minItems": 1 + }, + "cacheTagsHeader": { + "type": "string" + }, + "maxSize": { + "type": "integer" + }, + "maxEntrySize": { + "type": "integer" + }, + "maxCount": { + "type": "integer" + } + } + } + ] + }, + "watch": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "managementApi": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "type": "object", + "properties": { + "logs": { + "type": "object", + "properties": { + "maxSize": { + "type": "number", + "minimum": 5, + "default": 200 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "default": true + }, + "metrics": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "port": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "hostname": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "auth": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "username", + "password" + ] + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "readiness": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "endpoint": { + "type": "string" + }, + "success": { + "type": "object", + "properties": { + "statusCode": { + "type": "number" + }, + "body": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fail": { + "type": "object", + "properties": { + "statusCode": { + "type": "number" + }, + "body": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "liveness": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "endpoint": { + "type": "string" + }, + "success": { + "type": "object", + "properties": { + "statusCode": { + "type": "number" + }, + "body": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fail": { + "type": "object", + "properties": { + "statusCode": { + "type": "number" + }, + "body": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "additionalProperties": false + } + } + ] + }, + "telemetry": { + "type": "object", + "properties": { + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "serviceName": { + "type": "string", + "description": "The name of the service. Defaults to the folder name if not specified." + }, + "version": { + "type": "string", + "description": "The version of the service (optional)" + }, + "skip": { + "type": "array", + "description": "An array of paths to skip when creating spans. Useful for health checks and other endpoints that do not need to be traced.", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The path to skip. Can be a string or a regex." + }, + "method": { + "description": "HTTP method to skip", + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS" + ] + } + } + } + }, + "exporter": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "console", + "otlp", + "zipkin", + "memory", + "file" + ], + "default": "console" + }, + "options": { + "type": "object", + "description": "Options for the exporter. These are passed directly to the exporter.", + "properties": { + "url": { + "type": "string", + "description": "The URL to send the traces to. Not used for console or memory exporters." + }, + "headers": { + "type": "object", + "description": "Headers to send to the exporter. Not used for console or memory exporters." + }, + "path": { + "type": "string", + "description": "The path to write the traces to. Only for file exporter." + } + } + }, + "additionalProperties": false + } + } + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "console", + "otlp", + "zipkin", + "memory", + "file" + ], + "default": "console" + }, + "options": { + "type": "object", + "description": "Options for the exporter. These are passed directly to the exporter.", + "properties": { + "url": { + "type": "string", + "description": "The URL to send the traces to. Not used for console or memory exporters." + }, + "headers": { + "type": "object", + "description": "Headers to send to the exporter. Not used for console or memory exporters." + }, + "path": { + "type": "string", + "description": "The path to write the traces to. Only for file exporter." + } + } + }, + "additionalProperties": false + } + } + ] + } + }, + "required": [ + "serviceName" + ], + "additionalProperties": false + }, + "inspectorOptions": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "breakFirstLine": { + "type": "boolean" + }, + "watchDisabled": { + "type": "boolean" + } + } + }, + "serviceTimeout": { + "anyOf": [ + { + "type": "number", + "minimum": 1 + }, + { + "type": "string" + } + ], + "default": 300000 + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "sourceMaps": { + "type": "boolean", + "default": false + }, + "scheduler": { + "type": "array", + "items": { + "type": "object", + "properties": { + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ], + "default": true + }, + "name": { + "type": "string" + }, + "cron": { + "type": "string" + }, + "callbackUrl": { + "type": "string" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": true + } + ] + }, + "maxRetries": { + "type": "number", + "minimum": 0, + "default": 3 + } + }, + "required": [ + "name", + "cron", + "callbackUrl" + ] + } + } + }, + "additionalProperties": false + }, + "php": { + "type": "object", + "properties": { + "docroot": { + "type": "string", + "description": "Path to the root of the PHP project", + "resolvePath": true + }, + "rewriter": { + "type": "array", + "description": "A sequence of conditional rewrites to apply to PHP requests", + "items": { + "type": "object", + "description": "A single conditional rewrite to apply to PHP requests", + "properties": { + "operation": { + "type": "string", + "enum": [ + "and", + "or" + ], + "description": "Logical operation with which to group conditions" + }, + "conditions": { + "type": "array", + "description": "Conditions to match before applying rewrites", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "rewriters": { + "type": "array", + "description": "Set of rewrites to apply if conditions are met", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "required": [ + "rewriters" + ] + } } }, "required": [ diff --git a/test/plugin.test.js b/test/plugin.test.js index e2a9af1..9480fd9 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -11,7 +11,8 @@ async function startStackable (t, docroot = join(import.meta.dirname, './fixture $schema: '../../schema.json', module: '../../lib/index.js', php: { - docroot + docroot, + rewriter: opts.rewriter }, port: 0, server: { @@ -78,3 +79,18 @@ test('404', async t => { t.assert.deepStrictEqual(res.statusCode, 404) }) + +test('support rewriter', async t => { + const server = await startStackable(t, undefined, { + rewriter: [{ + rewriters: [{ + type: 'href', + args: ['^/(.*)$', '/index.php'] + }] + }] + }) + const res = await server.inject('/rewrite_me') + + t.assert.deepStrictEqual(res.statusCode, 200) + t.assert.deepStrictEqual(res.body, 'Hello World!') +})