organizations 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ca06945acfae769816fdc7c19ba44778791697325d1f59967fb4794c7b8a274
4
- data.tar.gz: 9601aa8b0835819532b1aed0a79a410ef4bf51f023b90088c463950dff5616c0
3
+ metadata.gz: bc99dabb60fe5f7d0c4ae8567fd62c4920f4a403b804dfe4e8dcac4d3c66fa11
4
+ data.tar.gz: '024976f0f5cd19fd8530179977a34e21d7ca54968bbf62d50eeff0d5f70dc1ea'
5
5
  SHA512:
6
- metadata.gz: 666c1b825cb64d0d822d654bf1db23c6672b993e7769369d43b0afb01480e13218511b1bd7da4c100b5761360049f9408c6c74a33a177a8e86b34ce6cab27efa
7
- data.tar.gz: 6416fb315eb949d06672665f5909bcf463976d265a05bbcdbc508978f5f20c6ccf9367c64c8a94ccbe1e8da57eeb934b3d12354dd9adb40ed51f00a9421f2883
6
+ metadata.gz: b14290ba7ecfc72544a5b4553fc5fd633ab51af9bcf1e3e1ed2bd90769ce2099a658f815191f1cf8c5e4706978d0070b80654e6b6da978b636ba6a87e60e11c9
7
+ data.tar.gz: 07bb21cfe8a870b8f672069ac7cb3f9512dad17ad1ea472b4cc34db9eaaf292773f05c6eb105dc5e220dc3b4b2139bfcdee1d097201774ca92cb79356373c8a3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [0.2.0] - 2026-02-20
2
+
3
+ - Namespaced all tables with `organizations_` prefix to prevent collisions with host apps
4
+
1
5
  ## [0.1.1] - 2026-02-19
2
6
 
3
7
  - Removed `slugifiable` dependency (deferred to host app)
data/README.md CHANGED
@@ -1064,9 +1064,9 @@ The gem provides `Organizations::Organization` as the base model. You can extend
1064
1064
  # db/migrate/xxx_add_custom_fields_to_organizations.rb
1065
1065
  class AddCustomFieldsToOrganizations < ActiveRecord::Migration[8.0]
1066
1066
  def change
1067
- add_column :organizations, :support_email, :string
1068
- add_column :organizations, :billing_address, :text
1069
- add_column :organizations, :settings, :jsonb, default: {}
1067
+ add_column :organizations_organizations, :support_email, :string
1068
+ add_column :organizations_organizations, :billing_address, :text
1069
+ add_column :organizations_organizations, :settings, :jsonb, default: {}
1070
1070
  end
1071
1071
  end
1072
1072
  ```
@@ -1110,10 +1110,10 @@ This is standard Rails practice — the gem provides the foundation (memberships
1110
1110
 
1111
1111
  The gem creates three tables:
1112
1112
 
1113
- ### organizations
1113
+ ### organizations_organizations
1114
1114
 
1115
1115
  ```sql
1116
- organizations
1116
+ organizations_organizations
1117
1117
  - id (primary key, auto-detects UUID or integer from your app)
1118
1118
  - name (string, required)
1119
1119
  - metadata (jsonb, default: {})
@@ -1123,10 +1123,10 @@ organizations
1123
1123
 
1124
1124
  > **Note:** The gem automatically detects your app's primary key type (UUID or integer) and uses it for all tables.
1125
1125
 
1126
- ### memberships
1126
+ ### organizations_memberships
1127
1127
 
1128
1128
  ```sql
1129
- memberships
1129
+ organizations_memberships
1130
1130
  - id (primary key)
1131
1131
  - user_id (foreign key, indexed)
1132
1132
  - organization_id (foreign key, indexed)
@@ -1138,10 +1138,10 @@ memberships
1138
1138
  unique index: [user_id, organization_id]
1139
1139
  ```
1140
1140
 
1141
- ### organization_invitations
1141
+ ### organizations_invitations
1142
1142
 
1143
1143
  ```sql
1144
- organization_invitations
1144
+ organizations_invitations
1145
1145
  - id (primary key)
1146
1146
  - organization_id (foreign key, indexed)
1147
1147
  - email (string, required, indexed)
@@ -1254,7 +1254,7 @@ If you display member counts frequently (pricing pages, org listings), consider
1254
1254
 
1255
1255
  ```ruby
1256
1256
  # In a migration
1257
- add_column :organizations, :memberships_count, :integer, default: 0, null: false
1257
+ add_column :organizations_organizations, :memberships_count, :integer, default: 0, null: false
1258
1258
 
1259
1259
  # Reset existing counts
1260
1260
  Organization.find_each do |org|
@@ -1275,9 +1275,9 @@ org.member_count
1275
1275
  Boolean checks use efficient SQL `EXISTS` queries:
1276
1276
 
1277
1277
  ```ruby
1278
- user.belongs_to_any_organization? # SELECT 1 FROM memberships WHERE ... LIMIT 1
1279
- user.has_pending_organization_invitations? # SELECT 1 FROM organization_invitations WHERE ... LIMIT 1
1280
- org.has_any_members? # SELECT 1 FROM memberships WHERE ... LIMIT 1
1278
+ user.belongs_to_any_organization? # SELECT 1 FROM organizations_memberships WHERE ... LIMIT 1
1279
+ user.has_pending_organization_invitations? # SELECT 1 FROM organizations_invitations WHERE ... LIMIT 1
1280
+ org.has_any_members? # SELECT 1 FROM organizations_memberships WHERE ... LIMIT 1
1281
1281
  ```
1282
1282
 
1283
1283
  ### Scoped associations use JOINs
@@ -1287,13 +1287,13 @@ Methods like `org.admins` and `user.owned_organizations` use proper SQL JOINs:
1287
1287
  ```ruby
1288
1288
  org.admins
1289
1289
  # SELECT users.* FROM users
1290
- # INNER JOIN memberships ON memberships.user_id = users.id
1291
- # WHERE memberships.organization_id = ? AND memberships.role IN ('admin', 'owner')
1290
+ # INNER JOIN organizations_memberships ON organizations_memberships.user_id = users.id
1291
+ # WHERE organizations_memberships.organization_id = ? AND organizations_memberships.role IN ('admin', 'owner')
1292
1292
 
1293
1293
  user.owned_organizations
1294
- # SELECT organizations.* FROM organizations
1295
- # INNER JOIN memberships ON memberships.organization_id = organizations.id
1296
- # WHERE memberships.user_id = ? AND memberships.role = 'owner'
1294
+ # SELECT organizations_organizations.* FROM organizations_organizations
1295
+ # INNER JOIN organizations_memberships ON organizations_memberships.organization_id = organizations_organizations.id
1296
+ # WHERE organizations_memberships.user_id = ? AND organizations_memberships.role = 'owner'
1297
1297
  ```
1298
1298
 
1299
1299
  ### Current organization memoization
@@ -1445,14 +1445,14 @@ The gem creates these indexes automatically:
1445
1445
 
1446
1446
  ```sql
1447
1447
  -- Fast membership lookups
1448
- CREATE UNIQUE INDEX index_memberships_on_user_and_org ON memberships (user_id, organization_id);
1449
- CREATE INDEX index_memberships_on_organization_id ON memberships (organization_id);
1450
- CREATE INDEX index_memberships_on_role ON memberships (role);
1448
+ CREATE UNIQUE INDEX index_organizations_memberships_on_user_and_org ON organizations_memberships (user_id, organization_id);
1449
+ CREATE INDEX index_organizations_memberships_on_organization_id ON organizations_memberships (organization_id);
1450
+ CREATE INDEX index_organizations_memberships_on_role ON organizations_memberships (role);
1451
1451
 
1452
1452
  -- Fast invitation lookups
1453
- CREATE UNIQUE INDEX index_invitations_on_token ON organization_invitations (token);
1454
- CREATE INDEX index_invitations_on_email ON organization_invitations (email);
1455
- CREATE UNIQUE INDEX index_invitations_pending ON organization_invitations (organization_id, LOWER(email)) WHERE accepted_at IS NULL;
1453
+ CREATE UNIQUE INDEX index_organizations_invitations_on_token ON organizations_invitations (token);
1454
+ CREATE INDEX index_organizations_invitations_on_email ON organizations_invitations (email);
1455
+ CREATE UNIQUE INDEX index_organizations_invitations_pending ON organizations_invitations (organization_id, LOWER(email)) WHERE accepted_at IS NULL;
1456
1456
  ```
1457
1457
 
1458
1458
  ## Migration from 1:1 relationships
@@ -6,7 +6,7 @@ class CreateOrganizationsTables < ActiveRecord::Migration<%= migration_version %
6
6
  adapter = connection.adapter_name.downcase
7
7
 
8
8
  # Organizations table
9
- create_table :organizations, id: primary_key_type do |t|
9
+ create_table :organizations_organizations, id: primary_key_type do |t|
10
10
  t.string :name, null: false
11
11
  t.send(json_column_type, :metadata, null: json_column_null, default: json_column_default)
12
12
 
@@ -14,9 +14,9 @@ class CreateOrganizationsTables < ActiveRecord::Migration<%= migration_version %
14
14
  end
15
15
 
16
16
  # Memberships join table (User ↔ Organization)
17
- create_table :memberships, id: primary_key_type do |t|
17
+ create_table :organizations_memberships, id: primary_key_type do |t|
18
18
  t.references :user, null: false, type: foreign_key_type, foreign_key: true
19
- t.references :organization, null: false, type: foreign_key_type, foreign_key: true
19
+ t.references :organization, null: false, type: foreign_key_type, foreign_key: { to_table: :organizations_organizations }
20
20
  t.references :invited_by, null: true, type: foreign_key_type, foreign_key: { to_table: :users }
21
21
  t.string :role, null: false, default: "member"
22
22
  t.send(json_column_type, :metadata, null: json_column_null, default: json_column_default)
@@ -24,27 +24,22 @@ class CreateOrganizationsTables < ActiveRecord::Migration<%= migration_version %
24
24
  t.timestamps
25
25
  end
26
26
 
27
- add_index :memberships, [:user_id, :organization_id], unique: true
28
- add_index :memberships, :role
27
+ add_index :organizations_memberships, [:user_id, :organization_id], unique: true
28
+ add_index :organizations_memberships, :role
29
29
 
30
30
  # Enforce "at most one owner membership per organization" at DB level where possible.
31
- if adapter.include?("postgresql")
31
+ # Both PostgreSQL and SQLite support partial indexes with identical syntax.
32
+ if adapter.include?("postgresql") || adapter.include?("sqlite")
32
33
  execute <<-SQL
33
- CREATE UNIQUE INDEX index_memberships_single_owner
34
- ON memberships (organization_id)
35
- WHERE role = 'owner'
36
- SQL
37
- elsif adapter.include?("sqlite")
38
- execute <<-SQL
39
- CREATE UNIQUE INDEX index_memberships_single_owner
40
- ON memberships (organization_id)
34
+ CREATE UNIQUE INDEX index_organizations_memberships_single_owner
35
+ ON organizations_memberships (organization_id)
41
36
  WHERE role = 'owner'
42
37
  SQL
43
38
  end
44
39
 
45
40
  # Invitations table
46
- create_table :organization_invitations, id: primary_key_type do |t|
47
- t.references :organization, null: false, type: foreign_key_type, foreign_key: true
41
+ create_table :organizations_invitations, id: primary_key_type do |t|
42
+ t.references :organization, null: false, type: foreign_key_type, foreign_key: { to_table: :organizations_organizations }
48
43
  # invited_by is nullable to support dependent: :nullify when user is deleted
49
44
  t.references :invited_by, null: true, type: foreign_key_type, foreign_key: { to_table: :users }
50
45
  t.string :email, null: false
@@ -56,29 +51,22 @@ class CreateOrganizationsTables < ActiveRecord::Migration<%= migration_version %
56
51
  t.timestamps
57
52
  end
58
53
 
59
- add_index :organization_invitations, :token, unique: true
60
- add_index :organization_invitations, :email
54
+ add_index :organizations_invitations, :token, unique: true
55
+ add_index :organizations_invitations, :email
61
56
 
62
57
  # Unique partial index: only one pending (non-accepted) invitation per email per org
63
- # Both PostgreSQL and SQLite support partial indexes
64
- if adapter.include?("postgresql")
65
- execute <<-SQL
66
- CREATE UNIQUE INDEX index_invitations_pending_unique
67
- ON organization_invitations (organization_id, LOWER(email))
68
- WHERE accepted_at IS NULL
69
- SQL
70
- elsif adapter.include?("sqlite")
71
- # SQLite supports partial indexes since 3.8.0
58
+ # Both PostgreSQL and SQLite (3.8.0+) support partial indexes with identical syntax.
59
+ if adapter.include?("postgresql") || adapter.include?("sqlite")
72
60
  execute <<-SQL
73
- CREATE UNIQUE INDEX index_invitations_pending_unique
74
- ON organization_invitations (organization_id, LOWER(email))
61
+ CREATE UNIQUE INDEX index_organizations_invitations_pending_unique
62
+ ON organizations_invitations (organization_id, LOWER(email))
75
63
  WHERE accepted_at IS NULL
76
64
  SQL
77
65
  elsif adapter.include?("mysql")
78
66
  # MySQL doesn't support partial indexes, so we use a generated column that is
79
67
  # only non-NULL for pending invitations and enforce uniqueness on that value.
80
68
  execute <<-SQL
81
- ALTER TABLE organization_invitations
69
+ ALTER TABLE organizations_invitations
82
70
  ADD COLUMN pending_email VARCHAR(255)
83
71
  GENERATED ALWAYS AS (
84
72
  CASE
@@ -88,10 +76,10 @@ class CreateOrganizationsTables < ActiveRecord::Migration<%= migration_version %
88
76
  ) STORED
89
77
  SQL
90
78
 
91
- add_index :organization_invitations, [:organization_id, :pending_email], unique: true, name: "index_invitations_pending_unique"
79
+ add_index :organizations_invitations, [:organization_id, :pending_email], unique: true, name: "index_organizations_invitations_pending_unique"
92
80
  else
93
81
  # For other adapters, fall back to app-level validation.
94
- add_index :organization_invitations, [:organization_id, :email], name: "index_invitations_on_org_and_email"
82
+ add_index :organizations_invitations, [:organization_id, :email], name: "index_organizations_invitations_on_org_and_email"
95
83
  end
96
84
  end
97
85
 
@@ -74,7 +74,7 @@ module Organizations
74
74
 
75
75
  # Organizations where user is owner (efficient JOIN)
76
76
  has_many :owned_organizations,
77
- -> { where(memberships: { role: "owner" }) },
77
+ -> { where(organizations_memberships: { role: "owner" }) },
78
78
  through: :memberships,
79
79
  source: :organization,
80
80
  class_name: "Organizations::Organization"
@@ -18,7 +18,7 @@ module Organizations
18
18
  # invitation.expired? # => false
19
19
  #
20
20
  class Invitation < ActiveRecord::Base
21
- self.table_name = "organization_invitations"
21
+ self.table_name = "organizations_invitations"
22
22
 
23
23
  # === Associations ===
24
24
 
@@ -16,7 +16,7 @@ module Organizations
16
16
  # membership.demote_to!(:member)
17
17
  #
18
18
  class Membership < ActiveRecord::Base
19
- self.table_name = "memberships"
19
+ self.table_name = "organizations_memberships"
20
20
 
21
21
  # Error raised when trying to demote below current role
22
22
  class CannotDemoteOwner < Organizations::Error; end
@@ -16,7 +16,7 @@ module Organizations
16
16
  # org.member_count # => 5
17
17
  #
18
18
  class Organization < ActiveRecord::Base
19
- self.table_name = "organizations"
19
+ self.table_name = "organizations_organizations"
20
20
 
21
21
  # Error raised when trying to perform invalid operations on organization
22
22
  class CannotRemoveOwner < Organizations::Error; end
@@ -54,7 +54,7 @@ module Organizations
54
54
  # @param user [User] The user
55
55
  # @return [ActiveRecord::Relation]
56
56
  scope :with_member, ->(user) {
57
- joins(:memberships).where(memberships: { user_id: user.id })
57
+ joins(:memberships).where(organizations_memberships: { user_id: user.id })
58
58
  }
59
59
 
60
60
  # === Member Query Methods ===
@@ -75,7 +75,7 @@ module Organizations
75
75
  # Uses efficient JOIN query to avoid N+1
76
76
  # @return [ActiveRecord::Relation<User>]
77
77
  def admins
78
- users.where(memberships: { role: %w[owner admin] }).distinct
78
+ users.where(organizations_memberships: { role: %w[owner admin] }).distinct
79
79
  end
80
80
 
81
81
  # Alias for users (semantic convenience)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Organizations
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -258,8 +258,8 @@ module Organizations
258
258
  memberships = user.memberships
259
259
  .includes(:organization)
260
260
  .joins(:organization)
261
- .select("memberships.id, memberships.organization_id, memberships.role, " \
262
- "organizations.id AS org_id, organizations.name AS org_name")
261
+ .select("organizations_memberships.id, organizations_memberships.organization_id, organizations_memberships.role, " \
262
+ "organizations_organizations.id AS org_id, organizations_organizations.name AS org_name")
263
263
 
264
264
  current_data = nil
265
265
  others = []
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: organizations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-02-19 00:00:00.000000000 Z
10
+ date: 2026-02-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: railties