feat(api): add tags to resource creation

Normalize tag names before attaching them, reject names that are too short
after sanitization, and return 404 when removing tags not attached to the
resource.

Adds a per-team unique tag-name index and migrates duplicate tags onto the
kept record before creating the constraint.
This commit is contained in:
Andras Bacsai
2026-07-07 13:56:33 +02:00
parent f617e58401
commit 11b35ba3c1
12 changed files with 1466 additions and 80 deletions
+675
View File
@@ -412,6 +412,13 @@
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the application."
},
"is_preserve_repository_enabled": {
"type": "boolean",
"default": false,
@@ -866,6 +873,13 @@
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the application."
},
"is_preserve_repository_enabled": {
"type": "boolean",
"default": false,
@@ -1320,6 +1334,13 @@
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the application."
},
"is_preserve_repository_enabled": {
"type": "boolean",
"default": false,
@@ -1678,6 +1699,13 @@
"type": "boolean",
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the application."
}
},
"type": "object"
@@ -2017,6 +2045,13 @@
"type": "boolean",
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the application."
}
},
"type": "object"
@@ -3720,6 +3755,179 @@
]
}
},
"\/applications\/{uuid}\/tags": {
"get": {
"tags": [
"Applications"
],
"summary": "List Tags",
"description": "List tags for an application by UUID.",
"operationId": "list-tags-by-application-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the application.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "List of tags.",
"content": {
"application\/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#\/components\/schemas\/Tag"
}
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
},
"post": {
"tags": [
"Applications"
],
"summary": "Create Tag",
"description": "Add tag(s) to an application by UUID.",
"operationId": "create-tag-by-application-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the application.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application\/json": {
"schema": {
"properties": {
"tag_name": {
"type": "string",
"description": "The tag name (min 2 characters). Required if tag_names is not provided."
},
"tag_names": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of tag names (each min 2 characters). Required if tag_name is not provided."
}
},
"type": "object"
}
}
}
},
"responses": {
"201": {
"description": "Tags added successfully.",
"content": {
"application\/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#\/components\/schemas\/Tag"
}
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/applications\/{uuid}\/tags\/{tag_uuid}": {
"delete": {
"tags": [
"Applications"
],
"summary": "Delete Tag",
"description": "Remove a tag from an application by UUID.",
"operationId": "delete-tag-by-application-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the application.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "tag_uuid",
"in": "path",
"description": "UUID of the tag.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Tag removed."
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/cloud-tokens": {
"get": {
"tags": [
@@ -5018,6 +5226,13 @@
"instant_deploy": {
"type": "boolean",
"description": "Instant deploy the database"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the database."
}
},
"type": "object"
@@ -5150,6 +5365,13 @@
"instant_deploy": {
"type": "boolean",
"description": "Instant deploy the database"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the database."
}
},
"type": "object"
@@ -5278,6 +5500,13 @@
"instant_deploy": {
"type": "boolean",
"description": "Instant deploy the database"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the database."
}
},
"type": "object"
@@ -5410,6 +5639,13 @@
"instant_deploy": {
"type": "boolean",
"description": "Instant deploy the database"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the database."
}
},
"type": "object"
@@ -5542,6 +5778,13 @@
"instant_deploy": {
"type": "boolean",
"description": "Instant deploy the database"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the database."
}
},
"type": "object"
@@ -5686,6 +5929,13 @@
"instant_deploy": {
"type": "boolean",
"description": "Instant deploy the database"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the database."
}
},
"type": "object"
@@ -5830,6 +6080,13 @@
"instant_deploy": {
"type": "boolean",
"description": "Instant deploy the database"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the database."
}
},
"type": "object"
@@ -5962,6 +6219,13 @@
"instant_deploy": {
"type": "boolean",
"description": "Instant deploy the database"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the database."
}
},
"type": "object"
@@ -7106,6 +7370,179 @@
]
}
},
"\/databases\/{uuid}\/tags": {
"get": {
"tags": [
"Databases"
],
"summary": "List Tags",
"description": "List tags for a database by UUID.",
"operationId": "list-tags-by-database-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the database.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "List of tags.",
"content": {
"application\/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#\/components\/schemas\/Tag"
}
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
},
"post": {
"tags": [
"Databases"
],
"summary": "Create Tag",
"description": "Add tag(s) to a database by UUID.",
"operationId": "create-tag-by-database-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the database.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application\/json": {
"schema": {
"properties": {
"tag_name": {
"type": "string",
"description": "The tag name (min 2 characters). Required if tag_names is not provided."
},
"tag_names": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of tag names (each min 2 characters). Required if tag_name is not provided."
}
},
"type": "object"
}
}
}
},
"responses": {
"201": {
"description": "Tags added successfully.",
"content": {
"application\/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#\/components\/schemas\/Tag"
}
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/databases\/{uuid}\/tags\/{tag_uuid}": {
"delete": {
"tags": [
"Databases"
],
"summary": "Delete Tag",
"description": "Remove a tag from a database by UUID.",
"operationId": "delete-tag-by-database-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the database.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "tag_uuid",
"in": "path",
"description": "UUID of the tag.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Tag removed."
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/deployments": {
"get": {
"tags": [
@@ -10968,6 +11405,13 @@
"type": "boolean",
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the service."
}
},
"type": "object"
@@ -12390,6 +12834,215 @@
]
}
},
"\/services\/{uuid}\/tags": {
"get": {
"tags": [
"Services"
],
"summary": "List Tags",
"description": "List tags for a service by UUID.",
"operationId": "list-tags-by-service-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the service.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "List of tags.",
"content": {
"application\/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#\/components\/schemas\/Tag"
}
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
},
"post": {
"tags": [
"Services"
],
"summary": "Create Tag",
"description": "Add tag(s) to a service by UUID.",
"operationId": "create-tag-by-service-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the service.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application\/json": {
"schema": {
"properties": {
"tag_name": {
"type": "string",
"description": "The tag name (min 2 characters). Required if tag_names is not provided."
},
"tag_names": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of tag names (each min 2 characters). Required if tag_name is not provided."
}
},
"type": "object"
}
}
}
},
"responses": {
"201": {
"description": "Tags added successfully.",
"content": {
"application\/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#\/components\/schemas\/Tag"
}
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/services\/{uuid}\/tags\/{tag_uuid}": {
"delete": {
"tags": [
"Services"
],
"summary": "Delete Tag",
"description": "Remove a tag from a service by UUID.",
"operationId": "delete-tag-by-service-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the service.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "tag_uuid",
"in": "path",
"description": "UUID of the tag.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Tag removed."
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/tags": {
"get": {
"tags": [
"Tags"
],
"summary": "List",
"description": "List all tags for the current team.",
"operationId": "list-tags",
"responses": {
"200": {
"description": "All tags for the current team.",
"content": {
"application\/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#\/components\/schemas\/Tag"
}
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/teams": {
"get": {
"tags": [
@@ -13610,6 +14263,24 @@
},
"type": "object"
},
"Tag": {
"description": "Tag model",
"properties": {
"uuid": {
"type": "string"
},
"name": {
"type": "string"
},
"created_at": {
"type": "string"
},
"updated_at": {
"type": "string"
}
},
"type": "object"
},
"Team": {
"description": "Team model",
"properties": {
@@ -13864,6 +14535,10 @@
"name": "Services",
"description": "Services"
},
{
"name": "Tags",
"description": "Tags"
},
{
"name": "Teams",
"description": "Teams"