Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ gem 'good_job', '~> 4.0'
gem 'rotp'

gem 'grpc', '~> 1.67'
gem 'tucana', '0.0.47'
gem 'tucana', '0.0.48'

gem 'code0-identities', '~> 0.0.2'

Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ GEM
thor (1.4.0)
timeout (0.4.4)
tsort (0.2.0)
tucana (0.0.47)
tucana (0.0.48)
grpc (~> 1.64)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
Expand Down Expand Up @@ -430,7 +430,7 @@ DEPENDENCIES
simplecov (~> 0.22.0)
simplecov-cobertura (~> 3.0)
test-prof (~> 1.0)
tucana (= 0.0.47)
tucana (= 0.0.48)
tzinfo-data

RUBY VERSION
Expand Down
1 change: 1 addition & 0 deletions app/graphql/mutations/namespaces/projects/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Create < BaseMutation

argument :description, String, required: false, description: 'Description for the new project.'
argument :name, String, required: true, description: 'Name for the new project.'
argument :slug, String, required: false, description: 'Slug for the new project.'

def resolve(namespace_id:, **params)
namespace = SagittariusSchema.object_from_id(namespace_id)
Expand Down
1 change: 1 addition & 0 deletions app/graphql/mutations/namespaces/projects/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Update < BaseMutation
argument :name, String, required: false, description: 'Name for the updated project.'
argument :primary_runtime_id, Types::GlobalIdType[::Runtime],
required: false, description: 'The primary runtime for the updated project.'
argument :slug, String, required: false, description: 'Slug for the updated project.'

def resolve(namespace_project_id:, **params)
project = SagittariusSchema.object_from_id(namespace_project_id)
Expand Down
2 changes: 2 additions & 0 deletions app/graphql/types/namespace_project_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class NamespaceProjectType < Types::BaseObject

field :name, String, null: false, description: 'Name of the project'

field :slug, String, null: false, description: 'Slug of the project used in URLs to identify flows'

field :runtimes, Types::RuntimeType.connection_type, null: false, description: 'Runtimes assigned to this project'

field :roles, Types::NamespaceRoleType.connection_type, null: false,
Expand Down
1 change: 1 addition & 0 deletions app/models/flow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def to_grpc
Tucana::Shared::ValidationFlow.new(
flow_id: id,
project_id: project.id,
project_slug: project.slug,
type: flow_type.identifier,
data_types: [], # TODO: when data types are creatable
input_type_identifier: input_type&.identifier,
Expand Down
6 changes: 6 additions & 0 deletions app/models/namespace_project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ class NamespaceProject < ApplicationRecord
source: :role
has_many :flows, class_name: 'Flow', inverse_of: :project

validates :slug, presence: true,
length: { minimum: 3, maximum: 50 },
allow_blank: false,
uniqueness: { case_sensitive: true },
format: { with: /\A[a-zA-Z0-9_-]+\z/ }

validates :name, presence: true,
length: { minimum: 3, maximum: 50 },
allow_blank: false,
Expand Down
11 changes: 11 additions & 0 deletions app/services/namespaces/projects/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ def execute
end

transactional do |t|
unless params.key?(:slug)
slug = name.parameterize

tries_left = 5
while NamespaceProject.exists?(slug: slug) && tries_left.positive?
slug = "#{slug}-#{SecureRandom.hex(4)}"
tries_left -= 1
end
params[:slug] = slug
end

project = NamespaceProject.create(namespace: namespace, name: name, **params)
unless project.persisted?
t.rollback_and_return! ServiceResponse.error(
Expand Down
1 change: 1 addition & 0 deletions db/fixtures/development/06_namespace_projects.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
NamespaceProject.seed_once :namespace_id, :name do |np|
np.namespace_id = Organization.find_by(name: 'Code1').ensure_namespace.id
np.name = 'First Project'
np.slug = 'first-project'
np.description = 'A sample project for Code1 organization.'
np.primary_runtime = Runtime.find_by(name: 'Code1-Runtime')
np.runtimes = [Runtime.find_by(name: 'Code1-Runtime')]
Expand Down
10 changes: 10 additions & 0 deletions db/migrate/20251215200029_add_slug_to_projects.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class AddSlugToProjects < Code0::ZeroTrack::Database::Migration[1.0]
def change
# rubocop:disable Rails/NotNullColumn -- backwards compatibility intentionally ignored
add_column :namespace_projects, :slug, :text, null: false
add_index :namespace_projects, :slug, unique: true
# rubocop:enable Rails/NotNullColumn
end
end
1 change: 1 addition & 0 deletions db/schema_migrations/20251215200029
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
10c61a28b25b5ae26834648b6b679e8946f8558faa5e4f18d909398495af654b
3 changes: 3 additions & 0 deletions db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ CREATE TABLE namespace_projects (
updated_at timestamp with time zone NOT NULL,
namespace_id bigint NOT NULL,
primary_runtime_id bigint,
slug text NOT NULL,
CONSTRAINT check_09e881e641 CHECK ((char_length(name) <= 50)),
CONSTRAINT check_a77bf7c685 CHECK ((char_length(description) <= 500))
);
Expand Down Expand Up @@ -1175,6 +1176,8 @@ CREATE INDEX index_namespace_projects_on_namespace_id ON namespace_projects USIN

CREATE INDEX index_namespace_projects_on_primary_runtime_id ON namespace_projects USING btree (primary_runtime_id);

CREATE UNIQUE INDEX index_namespace_projects_on_slug ON namespace_projects USING btree (slug);

CREATE INDEX index_namespace_role_project_assignments_on_project_id ON namespace_role_project_assignments USING btree (project_id);

CREATE UNIQUE INDEX "index_namespace_roles_on_namespace_id_LOWER_name" ON namespace_roles USING btree (namespace_id, lower(name));
Expand Down
1 change: 1 addition & 0 deletions docs/graphql/mutation/namespacesprojectscreate.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Creates a new namespace project.
| `description` | [`String`](../scalar/string.md) | Description for the new project. |
| `name` | [`String!`](../scalar/string.md) | Name for the new project. |
| `namespaceId` | [`NamespaceID!`](../scalar/namespaceid.md) | The id of the namespace which this project will belong to |
| `slug` | [`String`](../scalar/string.md) | Slug for the new project. |

## Fields

Expand Down
1 change: 1 addition & 0 deletions docs/graphql/mutation/namespacesprojectsupdate.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Updates a namespace project.
| `name` | [`String`](../scalar/string.md) | Name for the updated project. |
| `namespaceProjectId` | [`NamespaceProjectID!`](../scalar/namespaceprojectid.md) | The id of the namespace project which will be updated |
| `primaryRuntimeId` | [`RuntimeID`](../scalar/runtimeid.md) | The primary runtime for the updated project. |
| `slug` | [`String`](../scalar/string.md) | Slug for the updated project. |

## Fields

Expand Down
1 change: 1 addition & 0 deletions docs/graphql/object/namespaceproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Represents a namespace project
| `primaryRuntime` | [`Runtime`](../object/runtime.md) | The primary runtime for the project |
| `roles` | [`NamespaceRoleConnection!`](../object/namespaceroleconnection.md) | Roles assigned to this project |
| `runtimes` | [`RuntimeConnection!`](../object/runtimeconnection.md) | Runtimes assigned to this project |
| `slug` | [`String!`](../scalar/string.md) | Slug of the project used in URLs to identify flows |
| `updatedAt` | [`Time!`](../scalar/time.md) | Time when this NamespaceProject was last updated |
| `userAbilities` | [`NamespaceProjectUserAbilities!`](../object/namespaceprojectuserabilities.md) | Abilities for the current user on this NamespaceProject |

Expand Down
1 change: 1 addition & 0 deletions spec/factories/namespace_projects.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
factory :namespace_project do
namespace
name { generate(:namespace_project_name) }
slug { name.parameterize }
description { '' }
end
end
1 change: 1 addition & 0 deletions spec/graphql/types/namespace_project_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
%w[
id
name
slug
description
namespace
primary_runtime
Expand Down
10 changes: 6 additions & 4 deletions spec/models/flow_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@
let(:flow) do
create(
:flow,
flow_type: create(:flow_type, identifier: 'HTTP'),
flow_settings: [
create(
:flow_setting,
flow_setting_id: 'example_key',
object: { some_key: 'some_value' }
flow_setting_id: 'HTTP_URL',
object: { url: '/some-url' }
)
]
)
Expand Down Expand Up @@ -63,6 +64,7 @@
{
flow_id: flow.id,
project_id: flow.project.id,
project_slug: flow.project.slug,
type: flow.flow_type.identifier,
node_functions: [
{
Expand All @@ -88,8 +90,8 @@
flow_setting_id: flow.flow_settings.first.flow_setting_id,
object: {
fields: {
'some_key' => {
string_value: flow.flow_settings.first.object['some_key'],
'url' => {
string_value: flow.flow_settings.first.object['url'],
},
},
}
Expand Down
6 changes: 6 additions & 0 deletions spec/models/namespace_project_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).case_insensitive.scoped_to(:namespace_id) }
it { is_expected.to validate_length_of(:name).is_at_most(50) }
it { is_expected.to validate_presence_of(:slug) }
it { is_expected.to validate_uniqueness_of(:slug) }
it { is_expected.to validate_length_of(:slug).is_at_most(50) }
it { is_expected.to allow_value('valid-slug_123').for(:slug) }
it { is_expected.not_to allow_value('invalid slug!').for(:slug) }

it { is_expected.to validate_length_of(:description).is_at_most(500) }
it { is_expected.to allow_value(' ').for(:description) }
it { is_expected.to allow_value('').for(:description) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
author_id: current_user.id,
entity_id: namespace_project.id,
entity_type: 'NamespaceProject',
details: { name: input[:name] },
details: { name: input[:name], slug: namespace_project.slug },
target_id: namespace.id,
target_type: 'Namespace'
)
Expand Down Expand Up @@ -104,7 +104,7 @@
author_id: current_user.id,
entity_id: namespace_project.id,
entity_type: 'NamespaceProject',
details: { name: input[:name] },
details: { name: input[:name], slug: namespace_project.slug },
target_id: namespace.id,
target_type: 'Namespace'
)
Expand Down
33 changes: 33 additions & 0 deletions spec/services/namespaces/projects/create_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,43 @@
entity_type: 'NamespaceProject',
details: {
name: params[:name],
slug: service_response.payload.slug,
},
target_id: namespace.id,
target_type: 'Namespace'
)
end

context 'when slug is duplicated' do
let(:params) do
{ namespace: namespace, name: generate(:namespace_project_name), slug: 'some-slug' }
end

before do
create(:namespace_project, slug: 'some-slug')
end

it 'fails to create project' do
expect(service_response).to be_error
expect(service_response.payload[:error_code]).to eq(:invalid_namespace_project)
end

context 'when slug is not set' do
let(:params) do
{ namespace: namespace, name: generate(:namespace_project_name) }
end

before do
create(:namespace_project, slug: params[:name].parameterize)
end

it 'creates project with unique slug' do
expect(service_response).to be_success
expect(service_response.payload.reload).to be_valid
expect(service_response.payload.slug).not_to eq(params[:name].parameterize)
expect(service_response.payload.slug).to match(/^#{params[:name].parameterize}-[a-f0-9]{8}$/)
end
end
end
end
end