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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +24 -24
- data/lib/generators/organizations/install/templates/create_organizations_tables.rb.erb +20 -32
- data/lib/organizations/models/concerns/has_organizations.rb +1 -1
- data/lib/organizations/models/invitation.rb +1 -1
- data/lib/organizations/models/membership.rb +1 -1
- data/lib/organizations/models/organization.rb +3 -3
- data/lib/organizations/version.rb +1 -1
- data/lib/organizations/view_helpers.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc99dabb60fe5f7d0c4ae8567fd62c4920f4a403b804dfe4e8dcac4d3c66fa11
|
|
4
|
+
data.tar.gz: '024976f0f5cd19fd8530179977a34e21d7ca54968bbf62d50eeff0d5f70dc1ea'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b14290ba7ecfc72544a5b4553fc5fd633ab51af9bcf1e3e1ed2bd90769ce2099a658f815191f1cf8c5e4706978d0070b80654e6b6da978b636ba6a87e60e11c9
|
|
7
|
+
data.tar.gz: 07bb21cfe8a870b8f672069ac7cb3f9512dad17ad1ea472b4cc34db9eaaf292773f05c6eb105dc5e220dc3b4b2139bfcdee1d097201774ca92cb79356373c8a3
|
data/CHANGELOG.md
CHANGED
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 :
|
|
1068
|
-
add_column :
|
|
1069
|
-
add_column :
|
|
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
|
-
###
|
|
1113
|
+
### organizations_organizations
|
|
1114
1114
|
|
|
1115
1115
|
```sql
|
|
1116
|
-
|
|
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
|
-
###
|
|
1126
|
+
### organizations_memberships
|
|
1127
1127
|
|
|
1128
1128
|
```sql
|
|
1129
|
-
|
|
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
|
-
###
|
|
1141
|
+
### organizations_invitations
|
|
1142
1142
|
|
|
1143
1143
|
```sql
|
|
1144
|
-
|
|
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 :
|
|
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
|
|
1279
|
-
user.has_pending_organization_invitations? # SELECT 1 FROM
|
|
1280
|
-
org.has_any_members? # SELECT 1 FROM
|
|
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
|
|
1291
|
-
# WHERE
|
|
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
|
|
1295
|
-
# INNER JOIN
|
|
1296
|
-
# WHERE
|
|
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
|
|
1449
|
-
CREATE INDEX
|
|
1450
|
-
CREATE INDEX
|
|
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
|
|
1454
|
-
CREATE INDEX
|
|
1455
|
-
CREATE UNIQUE INDEX
|
|
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 :
|
|
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 :
|
|
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:
|
|
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 :
|
|
28
|
-
add_index :
|
|
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
|
-
|
|
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
|
|
34
|
-
ON
|
|
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 :
|
|
47
|
-
t.references :organization, null: false, type: foreign_key_type, foreign_key:
|
|
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 :
|
|
60
|
-
add_index :
|
|
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
|
|
74
|
-
ON
|
|
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
|
|
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 :
|
|
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 :
|
|
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(
|
|
77
|
+
-> { where(organizations_memberships: { role: "owner" }) },
|
|
78
78
|
through: :memberships,
|
|
79
79
|
source: :organization,
|
|
80
80
|
class_name: "Organizations::Organization"
|
|
@@ -16,7 +16,7 @@ module Organizations
|
|
|
16
16
|
# membership.demote_to!(:member)
|
|
17
17
|
#
|
|
18
18
|
class Membership < ActiveRecord::Base
|
|
19
|
-
self.table_name = "
|
|
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 = "
|
|
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(
|
|
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(
|
|
78
|
+
users.where(organizations_memberships: { role: %w[owner admin] }).distinct
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# Alias for users (semantic convenience)
|
|
@@ -258,8 +258,8 @@ module Organizations
|
|
|
258
258
|
memberships = user.memberships
|
|
259
259
|
.includes(:organization)
|
|
260
260
|
.joins(:organization)
|
|
261
|
-
.select("
|
|
262
|
-
"
|
|
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.
|
|
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-
|
|
10
|
+
date: 2026-02-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: railties
|