feat(api): add database environment variable management endpoints

Add CRUD API endpoints for managing environment variables on databases:
- GET /databases/{uuid}/envs - list environment variables
- POST /databases/{uuid}/envs - create environment variable
- PATCH /databases/{uuid}/envs - update environment variable
- PATCH /databases/{uuid}/envs/bulk - bulk create environment variables
- DELETE /databases/{uuid}/envs/{env_uuid} - delete environment variable

Includes comprehensive test suite and OpenAPI documentation.
This commit is contained in:
Andras Bacsai 2026-03-19 23:29:50 +01:00
parent fb76b68c08
commit c00d5de03e
5 changed files with 1517 additions and 0 deletions

View file

@ -11,6 +11,7 @@
use App\Http\Controllers\Controller;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DeleteResourceJob;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
@ -2750,4 +2751,551 @@ public function action_restart(Request $request)
200
);
}
private function removeSensitiveEnvData($env)
{
$env->makeHidden([
'id',
'resourceable',
'resourceable_id',
'resourceable_type',
]);
if (request()->attributes->get('can_read_sensitive', false) === false) {
$env->makeHidden([
'value',
'real_value',
]);
}
return serializeApiResponse($env);
}
#[OA\Get(
summary: 'List Envs',
description: 'List all envs by database UUID.',
path: '/databases/{uuid}/envs',
operationId: 'list-envs-by-database-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Databases'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the database.',
required: true,
schema: new OA\Schema(
type: 'string',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Environment variables.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function envs(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
if (! $database) {
return response()->json(['message' => 'Database not found.'], 404);
}
$this->authorize('view', $database);
$envs = $database->environment_variables->map(function ($env) {
return $this->removeSensitiveEnvData($env);
});
return response()->json($envs);
}
#[OA\Patch(
summary: 'Update Env',
description: 'Update env by database UUID.',
path: '/databases/{uuid}/envs',
operationId: 'update-env-by-database-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Databases'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the database.',
required: true,
schema: new OA\Schema(
type: 'string',
)
),
],
requestBody: new OA\RequestBody(
description: 'Env updated.',
required: true,
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['key', 'value'],
properties: [
'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
],
),
),
],
),
responses: [
new OA\Response(
response: 201,
description: 'Environment variable updated.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
ref: '#/components/schemas/EnvironmentVariable'
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
new OA\Response(
response: 422,
ref: '#/components/responses/422',
),
]
)]
public function update_env_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$database = queryDatabaseByUuidWithinTeam($request->route('uuid'), $teamId);
if (! $database) {
return response()->json(['message' => 'Database not found.'], 404);
}
$this->authorize('manageEnvironment', $database);
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
'is_shown_once' => 'boolean',
'comment' => 'string|nullable|max:256',
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$key = str($request->key)->trim()->replace(' ', '_')->value;
$env = $database->environment_variables()->where('key', $key)->first();
if (! $env) {
return response()->json(['message' => 'Environment variable not found.'], 404);
}
$env->value = $request->value;
if ($request->has('is_literal')) {
$env->is_literal = $request->is_literal;
}
if ($request->has('is_multiline')) {
$env->is_multiline = $request->is_multiline;
}
if ($request->has('is_shown_once')) {
$env->is_shown_once = $request->is_shown_once;
}
if ($request->has('comment')) {
$env->comment = $request->comment;
}
$env->save();
return response()->json($this->removeSensitiveEnvData($env))->setStatusCode(201);
}
#[OA\Patch(
summary: 'Update Envs (Bulk)',
description: 'Update multiple envs by database UUID.',
path: '/databases/{uuid}/envs/bulk',
operationId: 'update-envs-by-database-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Databases'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the database.',
required: true,
schema: new OA\Schema(
type: 'string',
)
),
],
requestBody: new OA\RequestBody(
description: 'Bulk envs updated.',
required: true,
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['data'],
properties: [
'data' => [
'type' => 'array',
'items' => new OA\Schema(
type: 'object',
properties: [
'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
],
),
],
],
),
),
],
),
responses: [
new OA\Response(
response: 201,
description: 'Environment variables updated.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
new OA\Response(
response: 422,
ref: '#/components/responses/422',
),
]
)]
public function create_bulk_envs(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$database = queryDatabaseByUuidWithinTeam($request->route('uuid'), $teamId);
if (! $database) {
return response()->json(['message' => 'Database not found.'], 404);
}
$this->authorize('manageEnvironment', $database);
$bulk_data = $request->get('data');
if (! $bulk_data) {
return response()->json(['message' => 'Bulk data is required.'], 400);
}
$updatedEnvs = collect();
foreach ($bulk_data as $item) {
$validator = customApiValidator($item, [
'key' => 'string|required',
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
'is_shown_once' => 'boolean',
'comment' => 'string|nullable|max:256',
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$key = str($item['key'])->trim()->replace(' ', '_')->value;
$env = $database->environment_variables()->updateOrCreate(
['key' => $key],
$item
);
$updatedEnvs->push($this->removeSensitiveEnvData($env));
}
return response()->json($updatedEnvs)->setStatusCode(201);
}
#[OA\Post(
summary: 'Create Env',
description: 'Create env by database UUID.',
path: '/databases/{uuid}/envs',
operationId: 'create-env-by-database-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Databases'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the database.',
required: true,
schema: new OA\Schema(
type: 'string',
)
),
],
requestBody: new OA\RequestBody(
required: true,
description: 'Env created.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Environment variable created.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'example' => 'nc0k04gk8g0cgsk440g0koko'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
new OA\Response(
response: 422,
ref: '#/components/responses/422',
),
]
)]
public function create_env(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$database = queryDatabaseByUuidWithinTeam($request->route('uuid'), $teamId);
if (! $database) {
return response()->json(['message' => 'Database not found.'], 404);
}
$this->authorize('manageEnvironment', $database);
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
'is_shown_once' => 'boolean',
'comment' => 'string|nullable|max:256',
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$key = str($request->key)->trim()->replace(' ', '_')->value;
$existingEnv = $database->environment_variables()->where('key', $key)->first();
if ($existingEnv) {
return response()->json([
'message' => 'Environment variable already exists. Use PATCH request to update it.',
], 409);
}
$env = $database->environment_variables()->create([
'key' => $key,
'value' => $request->value,
'is_literal' => $request->is_literal ?? false,
'is_multiline' => $request->is_multiline ?? false,
'is_shown_once' => $request->is_shown_once ?? false,
'comment' => $request->comment ?? null,
]);
return response()->json($this->removeSensitiveEnvData($env))->setStatusCode(201);
}
#[OA\Delete(
summary: 'Delete Env',
description: 'Delete env by UUID.',
path: '/databases/{uuid}/envs/{env_uuid}',
operationId: 'delete-env-by-database-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Databases'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the database.',
required: true,
schema: new OA\Schema(
type: 'string',
)
),
new OA\Parameter(
name: 'env_uuid',
in: 'path',
description: 'UUID of the environment variable.',
required: true,
schema: new OA\Schema(
type: 'string',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Environment variable deleted.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Environment variable deleted.'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function delete_env_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$database = queryDatabaseByUuidWithinTeam($request->route('uuid'), $teamId);
if (! $database) {
return response()->json(['message' => 'Database not found.'], 404);
}
$this->authorize('manageEnvironment', $database);
$env = EnvironmentVariable::where('uuid', $request->route('env_uuid'))
->where('resourceable_type', get_class($database))
->where('resourceable_id', $database->id)
->first();
if (! $env) {
return response()->json(['message' => 'Environment variable not found.'], 404);
}
$env->forceDelete();
return response()->json(['message' => 'Environment variable deleted.']);
}
}

View file

@ -6132,6 +6132,387 @@
]
}
},
"\/databases\/{uuid}\/envs": {
"get": {
"tags": [
"Databases"
],
"summary": "List Envs",
"description": "List all envs by database UUID.",
"operationId": "list-envs-by-database-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the database.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Environment variables.",
"content": {
"application\/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#\/components\/schemas\/EnvironmentVariable"
}
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
},
"post": {
"tags": [
"Databases"
],
"summary": "Create Env",
"description": "Create env by database UUID.",
"operationId": "create-env-by-database-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the database.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Env created.",
"required": true,
"content": {
"application\/json": {
"schema": {
"properties": {
"key": {
"type": "string",
"description": "The key of the environment variable."
},
"value": {
"type": "string",
"description": "The value of the environment variable."
},
"is_literal": {
"type": "boolean",
"description": "The flag to indicate if the environment variable is a literal, nothing espaced."
},
"is_multiline": {
"type": "boolean",
"description": "The flag to indicate if the environment variable is multiline."
},
"is_shown_once": {
"type": "boolean",
"description": "The flag to indicate if the environment variable's value is shown on the UI."
}
},
"type": "object"
}
}
}
},
"responses": {
"201": {
"description": "Environment variable created.",
"content": {
"application\/json": {
"schema": {
"properties": {
"uuid": {
"type": "string",
"example": "nc0k04gk8g0cgsk440g0koko"
}
},
"type": "object"
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
},
"patch": {
"tags": [
"Databases"
],
"summary": "Update Env",
"description": "Update env by database UUID.",
"operationId": "update-env-by-database-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the database.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Env updated.",
"required": true,
"content": {
"application\/json": {
"schema": {
"required": [
"key",
"value"
],
"properties": {
"key": {
"type": "string",
"description": "The key of the environment variable."
},
"value": {
"type": "string",
"description": "The value of the environment variable."
},
"is_literal": {
"type": "boolean",
"description": "The flag to indicate if the environment variable is a literal, nothing espaced."
},
"is_multiline": {
"type": "boolean",
"description": "The flag to indicate if the environment variable is multiline."
},
"is_shown_once": {
"type": "boolean",
"description": "The flag to indicate if the environment variable's value is shown on the UI."
}
},
"type": "object"
}
}
}
},
"responses": {
"201": {
"description": "Environment variable updated.",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/EnvironmentVariable"
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/databases\/{uuid}\/envs\/bulk": {
"patch": {
"tags": [
"Databases"
],
"summary": "Update Envs (Bulk)",
"description": "Update multiple envs by database UUID.",
"operationId": "update-envs-by-database-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the database.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Bulk envs updated.",
"required": true,
"content": {
"application\/json": {
"schema": {
"required": [
"data"
],
"properties": {
"data": {
"type": "array",
"items": {
"properties": {
"key": {
"type": "string",
"description": "The key of the environment variable."
},
"value": {
"type": "string",
"description": "The value of the environment variable."
},
"is_literal": {
"type": "boolean",
"description": "The flag to indicate if the environment variable is a literal, nothing espaced."
},
"is_multiline": {
"type": "boolean",
"description": "The flag to indicate if the environment variable is multiline."
},
"is_shown_once": {
"type": "boolean",
"description": "The flag to indicate if the environment variable's value is shown on the UI."
}
},
"type": "object"
}
}
},
"type": "object"
}
}
}
},
"responses": {
"201": {
"description": "Environment variables updated.",
"content": {
"application\/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#\/components\/schemas\/EnvironmentVariable"
}
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/databases\/{uuid}\/envs\/{env_uuid}": {
"delete": {
"tags": [
"Databases"
],
"summary": "Delete Env",
"description": "Delete env by UUID.",
"operationId": "delete-env-by-database-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the database.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "env_uuid",
"in": "path",
"description": "UUID of the environment variable.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Environment variable deleted.",
"content": {
"application\/json": {
"schema": {
"properties": {
"message": {
"type": "string",
"example": "Environment variable deleted."
}
},
"type": "object"
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/deployments": {
"get": {
"tags": [

View file

@ -3973,6 +3973,242 @@ paths:
security:
-
bearerAuth: []
'/databases/{uuid}/envs':
get:
tags:
- Databases
summary: 'List Envs'
description: 'List all envs by database UUID.'
operationId: list-envs-by-database-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the database.'
required: true
schema:
type: string
responses:
'200':
description: 'Environment variables.'
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/EnvironmentVariable'
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
post:
tags:
- Databases
summary: 'Create Env'
description: 'Create env by database UUID.'
operationId: create-env-by-database-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the database.'
required: true
schema:
type: string
requestBody:
description: 'Env created.'
required: true
content:
application/json:
schema:
properties:
key:
type: string
description: 'The key of the environment variable.'
value:
type: string
description: 'The value of the environment variable.'
is_literal:
type: boolean
description: 'The flag to indicate if the environment variable is a literal, nothing espaced.'
is_multiline:
type: boolean
description: 'The flag to indicate if the environment variable is multiline.'
is_shown_once:
type: boolean
description: "The flag to indicate if the environment variable's value is shown on the UI."
type: object
responses:
'201':
description: 'Environment variable created.'
content:
application/json:
schema:
properties:
uuid: { type: string, example: nc0k04gk8g0cgsk440g0koko }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
'422':
$ref: '#/components/responses/422'
security:
-
bearerAuth: []
patch:
tags:
- Databases
summary: 'Update Env'
description: 'Update env by database UUID.'
operationId: update-env-by-database-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the database.'
required: true
schema:
type: string
requestBody:
description: 'Env updated.'
required: true
content:
application/json:
schema:
required:
- key
- value
properties:
key:
type: string
description: 'The key of the environment variable.'
value:
type: string
description: 'The value of the environment variable.'
is_literal:
type: boolean
description: 'The flag to indicate if the environment variable is a literal, nothing espaced.'
is_multiline:
type: boolean
description: 'The flag to indicate if the environment variable is multiline.'
is_shown_once:
type: boolean
description: "The flag to indicate if the environment variable's value is shown on the UI."
type: object
responses:
'201':
description: 'Environment variable updated.'
content:
application/json:
schema:
$ref: '#/components/schemas/EnvironmentVariable'
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
'422':
$ref: '#/components/responses/422'
security:
-
bearerAuth: []
'/databases/{uuid}/envs/bulk':
patch:
tags:
- Databases
summary: 'Update Envs (Bulk)'
description: 'Update multiple envs by database UUID.'
operationId: update-envs-by-database-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the database.'
required: true
schema:
type: string
requestBody:
description: 'Bulk envs updated.'
required: true
content:
application/json:
schema:
required:
- data
properties:
data:
type: array
items: { properties: { key: { type: string, description: 'The key of the environment variable.' }, value: { type: string, description: 'The value of the environment variable.' }, is_literal: { type: boolean, description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' }, is_multiline: { type: boolean, description: 'The flag to indicate if the environment variable is multiline.' }, is_shown_once: { type: boolean, description: "The flag to indicate if the environment variable's value is shown on the UI." } }, type: object }
type: object
responses:
'201':
description: 'Environment variables updated.'
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/EnvironmentVariable'
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
'422':
$ref: '#/components/responses/422'
security:
-
bearerAuth: []
'/databases/{uuid}/envs/{env_uuid}':
delete:
tags:
- Databases
summary: 'Delete Env'
description: 'Delete env by UUID.'
operationId: delete-env-by-database-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the database.'
required: true
schema:
type: string
-
name: env_uuid
in: path
description: 'UUID of the environment variable.'
required: true
schema:
type: string
responses:
'200':
description: 'Environment variable deleted.'
content:
application/json:
schema:
properties:
message: { type: string, example: 'Environment variable deleted.' }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
/deployments:
get:
tags:

View file

@ -154,6 +154,12 @@
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}', [DatabasesController::class, 'delete_execution_by_uuid'])->middleware(['api.ability:write']);
Route::get('/databases/{uuid}/envs', [DatabasesController::class, 'envs'])->middleware(['api.ability:read']);
Route::post('/databases/{uuid}/envs', [DatabasesController::class, 'create_env'])->middleware(['api.ability:write']);
Route::patch('/databases/{uuid}/envs/bulk', [DatabasesController::class, 'create_bulk_envs'])->middleware(['api.ability:write']);
Route::patch('/databases/{uuid}/envs', [DatabasesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/databases/{uuid}/envs/{env_uuid}', [DatabasesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
Route::match(['get', 'post'], '/databases/{uuid}/stop', [DatabasesController::class, 'action_stop'])->middleware(['api.ability:deploy']);

View file

@ -0,0 +1,346 @@
<?php
use App\Models\Environment;
use App\Models\EnvironmentVariable;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::updateOrCreate(['id' => 0]);
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
session(['currentTeam' => $this->team]);
$this->token = $this->user->createToken('test-token', ['*']);
$this->bearerToken = $this->token->plainTextToken;
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
});
function createDatabase($context): StandalonePostgresql
{
return StandalonePostgresql::create([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
'postgres_password' => 'password',
'postgres_db' => 'postgres',
'environment_id' => $context->environment->id,
'destination_id' => $context->destination->id,
'destination_type' => $context->destination->getMorphClass(),
]);
}
describe('GET /api/v1/databases/{uuid}/envs', function () {
test('lists environment variables for a database', function () {
$database = createDatabase($this);
EnvironmentVariable::create([
'key' => 'CUSTOM_VAR',
'value' => 'custom_value',
'resourceable_type' => StandalonePostgresql::class,
'resourceable_id' => $database->id,
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->getJson("/api/v1/databases/{$database->uuid}/envs");
$response->assertStatus(200);
$response->assertJsonFragment(['key' => 'CUSTOM_VAR']);
});
test('returns empty array when no environment variables exist', function () {
$database = createDatabase($this);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->getJson("/api/v1/databases/{$database->uuid}/envs");
$response->assertStatus(200);
$response->assertJson([]);
});
test('returns 404 for non-existent database', function () {
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->getJson('/api/v1/databases/non-existent-uuid/envs');
$response->assertStatus(404);
});
});
describe('POST /api/v1/databases/{uuid}/envs', function () {
test('creates an environment variable', function () {
$database = createDatabase($this);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->postJson("/api/v1/databases/{$database->uuid}/envs", [
'key' => 'NEW_VAR',
'value' => 'new_value',
]);
$response->assertStatus(201);
$env = EnvironmentVariable::where('key', 'NEW_VAR')
->where('resourceable_id', $database->id)
->where('resourceable_type', StandalonePostgresql::class)
->first();
expect($env)->not->toBeNull();
expect($env->value)->toBe('new_value');
});
test('creates an environment variable with comment', function () {
$database = createDatabase($this);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->postJson("/api/v1/databases/{$database->uuid}/envs", [
'key' => 'COMMENTED_VAR',
'value' => 'some_value',
'comment' => 'This is a test comment',
]);
$response->assertStatus(201);
$env = EnvironmentVariable::where('key', 'COMMENTED_VAR')
->where('resourceable_id', $database->id)
->first();
expect($env->comment)->toBe('This is a test comment');
});
test('returns 409 when environment variable already exists', function () {
$database = createDatabase($this);
EnvironmentVariable::create([
'key' => 'EXISTING_VAR',
'value' => 'existing_value',
'resourceable_type' => StandalonePostgresql::class,
'resourceable_id' => $database->id,
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->postJson("/api/v1/databases/{$database->uuid}/envs", [
'key' => 'EXISTING_VAR',
'value' => 'new_value',
]);
$response->assertStatus(409);
});
test('returns 422 when key is missing', function () {
$database = createDatabase($this);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->postJson("/api/v1/databases/{$database->uuid}/envs", [
'value' => 'some_value',
]);
$response->assertStatus(422);
});
});
describe('PATCH /api/v1/databases/{uuid}/envs', function () {
test('updates an environment variable', function () {
$database = createDatabase($this);
EnvironmentVariable::create([
'key' => 'UPDATE_ME',
'value' => 'old_value',
'resourceable_type' => StandalonePostgresql::class,
'resourceable_id' => $database->id,
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->patchJson("/api/v1/databases/{$database->uuid}/envs", [
'key' => 'UPDATE_ME',
'value' => 'new_value',
]);
$response->assertStatus(201);
$env = EnvironmentVariable::where('key', 'UPDATE_ME')
->where('resourceable_id', $database->id)
->first();
expect($env->value)->toBe('new_value');
});
test('returns 404 when environment variable does not exist', function () {
$database = createDatabase($this);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->patchJson("/api/v1/databases/{$database->uuid}/envs", [
'key' => 'NONEXISTENT',
'value' => 'value',
]);
$response->assertStatus(404);
});
});
describe('PATCH /api/v1/databases/{uuid}/envs/bulk', function () {
test('creates environment variables with comments', function () {
$database = createDatabase($this);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->patchJson("/api/v1/databases/{$database->uuid}/envs/bulk", [
'data' => [
[
'key' => 'DB_HOST',
'value' => 'localhost',
'comment' => 'Database host',
],
[
'key' => 'DB_PORT',
'value' => '5432',
],
],
]);
$response->assertStatus(201);
$envWithComment = EnvironmentVariable::where('key', 'DB_HOST')
->where('resourceable_id', $database->id)
->where('resourceable_type', StandalonePostgresql::class)
->first();
$envWithoutComment = EnvironmentVariable::where('key', 'DB_PORT')
->where('resourceable_id', $database->id)
->where('resourceable_type', StandalonePostgresql::class)
->first();
expect($envWithComment->comment)->toBe('Database host');
expect($envWithoutComment->comment)->toBeNull();
});
test('updates existing environment variables via bulk', function () {
$database = createDatabase($this);
EnvironmentVariable::create([
'key' => 'BULK_VAR',
'value' => 'old_value',
'comment' => 'Old comment',
'resourceable_type' => StandalonePostgresql::class,
'resourceable_id' => $database->id,
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->patchJson("/api/v1/databases/{$database->uuid}/envs/bulk", [
'data' => [
[
'key' => 'BULK_VAR',
'value' => 'new_value',
'comment' => 'Updated comment',
],
],
]);
$response->assertStatus(201);
$env = EnvironmentVariable::where('key', 'BULK_VAR')
->where('resourceable_id', $database->id)
->first();
expect($env->value)->toBe('new_value');
expect($env->comment)->toBe('Updated comment');
});
test('rejects comment exceeding 256 characters', function () {
$database = createDatabase($this);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->patchJson("/api/v1/databases/{$database->uuid}/envs/bulk", [
'data' => [
[
'key' => 'TEST_VAR',
'value' => 'value',
'comment' => str_repeat('a', 257),
],
],
]);
$response->assertStatus(422);
});
test('returns 400 when data is missing', function () {
$database = createDatabase($this);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->patchJson("/api/v1/databases/{$database->uuid}/envs/bulk", []);
$response->assertStatus(400);
});
});
describe('DELETE /api/v1/databases/{uuid}/envs/{env_uuid}', function () {
test('deletes an environment variable', function () {
$database = createDatabase($this);
$env = EnvironmentVariable::create([
'key' => 'DELETE_ME',
'value' => 'to_delete',
'resourceable_type' => StandalonePostgresql::class,
'resourceable_id' => $database->id,
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->deleteJson("/api/v1/databases/{$database->uuid}/envs/{$env->uuid}");
$response->assertStatus(200);
$response->assertJson(['message' => 'Environment variable deleted.']);
expect(EnvironmentVariable::where('uuid', $env->uuid)->first())->toBeNull();
});
test('returns 404 for non-existent environment variable', function () {
$database = createDatabase($this);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->deleteJson("/api/v1/databases/{$database->uuid}/envs/non-existent-uuid");
$response->assertStatus(404);
});
});