@sudobility/entity_service 1.0.1

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.
Files changed (72) hide show
  1. package/CLAUDE.md +124 -0
  2. package/dist/helpers/EntityHelper.cjs +234 -0
  3. package/dist/helpers/EntityHelper.d.ts +60 -0
  4. package/dist/helpers/EntityHelper.d.ts.map +1 -0
  5. package/dist/helpers/EntityHelper.js +234 -0
  6. package/dist/helpers/EntityHelper.js.map +1 -0
  7. package/dist/helpers/EntityMemberHelper.cjs +215 -0
  8. package/dist/helpers/EntityMemberHelper.d.ts +45 -0
  9. package/dist/helpers/EntityMemberHelper.d.ts.map +1 -0
  10. package/dist/helpers/EntityMemberHelper.js +215 -0
  11. package/dist/helpers/EntityMemberHelper.js.map +1 -0
  12. package/dist/helpers/InvitationHelper.cjs +251 -0
  13. package/dist/helpers/InvitationHelper.d.ts +59 -0
  14. package/dist/helpers/InvitationHelper.d.ts.map +1 -0
  15. package/dist/helpers/InvitationHelper.js +251 -0
  16. package/dist/helpers/InvitationHelper.js.map +1 -0
  17. package/dist/helpers/PermissionHelper.cjs +197 -0
  18. package/dist/helpers/PermissionHelper.d.ts +86 -0
  19. package/dist/helpers/PermissionHelper.d.ts.map +1 -0
  20. package/dist/helpers/PermissionHelper.js +197 -0
  21. package/dist/helpers/PermissionHelper.js.map +1 -0
  22. package/dist/helpers/index.cjs +15 -0
  23. package/dist/helpers/index.d.ts +8 -0
  24. package/dist/helpers/index.d.ts.map +1 -0
  25. package/dist/helpers/index.js +15 -0
  26. package/dist/helpers/index.js.map +1 -0
  27. package/dist/index.cjs +76 -0
  28. package/dist/index.d.ts +36 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +76 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/middleware/hono.cjs +148 -0
  33. package/dist/middleware/hono.d.ts +102 -0
  34. package/dist/middleware/hono.d.ts.map +1 -0
  35. package/dist/middleware/hono.js +148 -0
  36. package/dist/middleware/hono.js.map +1 -0
  37. package/dist/middleware/index.cjs +12 -0
  38. package/dist/middleware/index.d.ts +5 -0
  39. package/dist/middleware/index.d.ts.map +1 -0
  40. package/dist/middleware/index.js +12 -0
  41. package/dist/middleware/index.js.map +1 -0
  42. package/dist/migrations/001_add_entities.cjs +269 -0
  43. package/dist/migrations/001_add_entities.d.ts +29 -0
  44. package/dist/migrations/001_add_entities.d.ts.map +1 -0
  45. package/dist/migrations/001_add_entities.js +269 -0
  46. package/dist/migrations/001_add_entities.js.map +1 -0
  47. package/dist/migrations/index.cjs +10 -0
  48. package/dist/migrations/index.d.ts +5 -0
  49. package/dist/migrations/index.d.ts.map +1 -0
  50. package/dist/migrations/index.js +10 -0
  51. package/dist/migrations/index.js.map +1 -0
  52. package/dist/schema/entities.cjs +304 -0
  53. package/dist/schema/entities.d.ts +1047 -0
  54. package/dist/schema/entities.d.ts.map +1 -0
  55. package/dist/schema/entities.js +304 -0
  56. package/dist/schema/entities.js.map +1 -0
  57. package/dist/types/index.cjs +14 -0
  58. package/dist/types/index.d.ts +71 -0
  59. package/dist/types/index.d.ts.map +1 -0
  60. package/dist/types/index.js +14 -0
  61. package/dist/types/index.js.map +1 -0
  62. package/dist/utils/index.cjs +21 -0
  63. package/dist/utils/index.d.ts +5 -0
  64. package/dist/utils/index.d.ts.map +1 -0
  65. package/dist/utils/index.js +21 -0
  66. package/dist/utils/index.js.map +1 -0
  67. package/dist/utils/slug-generator.cjs +92 -0
  68. package/dist/utils/slug-generator.d.ts +41 -0
  69. package/dist/utils/slug-generator.d.ts.map +1 -0
  70. package/dist/utils/slug-generator.js +92 -0
  71. package/dist/utils/slug-generator.js.map +1 -0
  72. package/package.json +78 -0
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Entity Tables Migration
4
+ * @description Creates entity tables and migrates existing data
5
+ *
6
+ * This migration:
7
+ * 1. Creates entities, entity_members, entity_invitations tables
8
+ * 2. Adds entity_id column to projects table (nullable for backward compatibility)
9
+ * 3. Creates personal entities for existing users
10
+ * 4. Populates entity_id for existing projects
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.runEntityMigration = runEntityMigration;
14
+ exports.rollbackEntityMigration = rollbackEntityMigration;
15
+ /**
16
+ * Run the full entity migration.
17
+ */
18
+ async function runEntityMigration(config) {
19
+ const { client, schemaName, indexPrefix, migrateProjects = true } = config;
20
+ const prefix = `${schemaName}.`;
21
+ console.log(`Running entity migration for schema: ${schemaName}`);
22
+ // Step 1: Create entity tables
23
+ await createEntityTables(client, prefix, indexPrefix);
24
+ // Step 2: Add entity_id to projects if requested
25
+ if (migrateProjects) {
26
+ await addEntityIdToProjects(client, prefix, indexPrefix);
27
+ }
28
+ // Step 3: Migrate existing users to personal entities
29
+ await migrateUsersToPersonalEntities(client, prefix);
30
+ // Step 4: Populate entity_id for existing projects
31
+ if (migrateProjects) {
32
+ await populateProjectEntityIds(client, prefix);
33
+ }
34
+ console.log('Entity migration completed successfully');
35
+ }
36
+ /**
37
+ * Create the entity tables.
38
+ */
39
+ async function createEntityTables(client, prefix, indexPrefix) {
40
+ console.log('Creating entity tables...');
41
+ // Create entities table
42
+ await client.unsafe(`
43
+ CREATE TABLE IF NOT EXISTS ${prefix}entities (
44
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
45
+ entity_slug VARCHAR(12) NOT NULL UNIQUE,
46
+ entity_type VARCHAR(20) NOT NULL CHECK (entity_type IN ('personal', 'organization')),
47
+ display_name VARCHAR(255) NOT NULL,
48
+ description TEXT,
49
+ avatar_url TEXT,
50
+ owner_user_id UUID NOT NULL,
51
+ created_at TIMESTAMPTZ DEFAULT NOW(),
52
+ updated_at TIMESTAMPTZ DEFAULT NOW()
53
+ )
54
+ `);
55
+ await client.unsafe(`
56
+ CREATE UNIQUE INDEX IF NOT EXISTS ${indexPrefix}_entities_slug_idx
57
+ ON ${prefix}entities (entity_slug)
58
+ `);
59
+ await client.unsafe(`
60
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entities_owner_idx
61
+ ON ${prefix}entities (owner_user_id)
62
+ `);
63
+ await client.unsafe(`
64
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entities_type_idx
65
+ ON ${prefix}entities (entity_type)
66
+ `);
67
+ // Create entity_members table
68
+ await client.unsafe(`
69
+ CREATE TABLE IF NOT EXISTS ${prefix}entity_members (
70
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
71
+ entity_id UUID NOT NULL REFERENCES ${prefix}entities(id) ON DELETE CASCADE,
72
+ user_id UUID NOT NULL,
73
+ role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'manager', 'viewer')),
74
+ joined_at TIMESTAMPTZ DEFAULT NOW(),
75
+ created_at TIMESTAMPTZ DEFAULT NOW(),
76
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
77
+ UNIQUE(entity_id, user_id)
78
+ )
79
+ `);
80
+ await client.unsafe(`
81
+ CREATE UNIQUE INDEX IF NOT EXISTS ${indexPrefix}_entity_members_entity_user_idx
82
+ ON ${prefix}entity_members (entity_id, user_id)
83
+ `);
84
+ await client.unsafe(`
85
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entity_members_entity_idx
86
+ ON ${prefix}entity_members (entity_id)
87
+ `);
88
+ await client.unsafe(`
89
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entity_members_user_idx
90
+ ON ${prefix}entity_members (user_id)
91
+ `);
92
+ // Create entity_invitations table
93
+ await client.unsafe(`
94
+ CREATE TABLE IF NOT EXISTS ${prefix}entity_invitations (
95
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
96
+ entity_id UUID NOT NULL REFERENCES ${prefix}entities(id) ON DELETE CASCADE,
97
+ email VARCHAR(255) NOT NULL,
98
+ role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'manager', 'viewer')),
99
+ status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined', 'expired')),
100
+ invited_by_user_id UUID NOT NULL,
101
+ token VARCHAR(64) NOT NULL UNIQUE,
102
+ expires_at TIMESTAMPTZ NOT NULL,
103
+ accepted_at TIMESTAMPTZ,
104
+ created_at TIMESTAMPTZ DEFAULT NOW(),
105
+ updated_at TIMESTAMPTZ DEFAULT NOW()
106
+ )
107
+ `);
108
+ await client.unsafe(`
109
+ CREATE UNIQUE INDEX IF NOT EXISTS ${indexPrefix}_entity_invitations_token_idx
110
+ ON ${prefix}entity_invitations (token)
111
+ `);
112
+ await client.unsafe(`
113
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entity_invitations_entity_idx
114
+ ON ${prefix}entity_invitations (entity_id)
115
+ `);
116
+ await client.unsafe(`
117
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entity_invitations_email_idx
118
+ ON ${prefix}entity_invitations (email)
119
+ `);
120
+ await client.unsafe(`
121
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entity_invitations_status_idx
122
+ ON ${prefix}entity_invitations (status)
123
+ `);
124
+ console.log('Entity tables created');
125
+ }
126
+ /**
127
+ * Add entity_id column to projects table.
128
+ */
129
+ async function addEntityIdToProjects(client, prefix, indexPrefix) {
130
+ console.log('Adding entity_id to projects table...');
131
+ // Check if column already exists
132
+ const columnExists = await client.unsafe(`
133
+ SELECT EXISTS (
134
+ SELECT 1 FROM information_schema.columns
135
+ WHERE table_schema = '${prefix.replace('.', '')}'
136
+ AND table_name = 'projects'
137
+ AND column_name = 'entity_id'
138
+ )
139
+ `);
140
+ if (!columnExists[0]?.exists) {
141
+ // Add nullable entity_id column
142
+ await client.unsafe(`
143
+ ALTER TABLE ${prefix}projects
144
+ ADD COLUMN entity_id UUID REFERENCES ${prefix}entities(id) ON DELETE CASCADE
145
+ `);
146
+ // Create index on entity_id
147
+ await client.unsafe(`
148
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_projects_entity_idx
149
+ ON ${prefix}projects (entity_id)
150
+ `);
151
+ console.log('entity_id column added to projects');
152
+ }
153
+ else {
154
+ console.log('entity_id column already exists in projects');
155
+ }
156
+ }
157
+ /**
158
+ * Create personal entities for existing users.
159
+ */
160
+ async function migrateUsersToPersonalEntities(client, prefix) {
161
+ console.log('Migrating users to personal entities...');
162
+ // Get users without personal entities
163
+ const usersWithoutEntities = await client.unsafe(`
164
+ SELECT u.id, u.email, u.display_name,
165
+ COALESCE(s.organization_path, SUBSTRING(MD5(RANDOM()::TEXT) FROM 1 FOR 8)) as slug_source
166
+ FROM ${prefix}users u
167
+ LEFT JOIN ${prefix}user_settings s ON u.id = s.user_id
168
+ WHERE NOT EXISTS (
169
+ SELECT 1 FROM ${prefix}entities e
170
+ WHERE e.owner_user_id = u.id AND e.entity_type = 'personal'
171
+ )
172
+ `);
173
+ let migratedCount = 0;
174
+ for (const user of usersWithoutEntities) {
175
+ // Generate a unique slug (8 chars, lowercase alphanumeric)
176
+ const slug = generateSlug(user.slug_source);
177
+ const displayName = user.display_name || user.email?.split('@')[0] || 'Personal';
178
+ try {
179
+ // Create personal entity
180
+ const [entity] = await client.unsafe(`
181
+ INSERT INTO ${prefix}entities (entity_slug, entity_type, display_name, owner_user_id)
182
+ VALUES ('${slug}', 'personal', '${displayName.replace(/'/g, "''")}', '${user.id}')
183
+ RETURNING id
184
+ `);
185
+ // Add user as admin member
186
+ await client.unsafe(`
187
+ INSERT INTO ${prefix}entity_members (entity_id, user_id, role)
188
+ VALUES ('${entity.id}', '${user.id}', 'admin')
189
+ `);
190
+ migratedCount++;
191
+ }
192
+ catch (error) {
193
+ // If slug collision, generate a new one and retry
194
+ if (error.code === '23505') {
195
+ const newSlug = generateSlug();
196
+ const [entity] = await client.unsafe(`
197
+ INSERT INTO ${prefix}entities (entity_slug, entity_type, display_name, owner_user_id)
198
+ VALUES ('${newSlug}', 'personal', '${displayName.replace(/'/g, "''")}', '${user.id}')
199
+ RETURNING id
200
+ `);
201
+ await client.unsafe(`
202
+ INSERT INTO ${prefix}entity_members (entity_id, user_id, role)
203
+ VALUES ('${entity.id}', '${user.id}', 'admin')
204
+ `);
205
+ migratedCount++;
206
+ }
207
+ else {
208
+ throw error;
209
+ }
210
+ }
211
+ }
212
+ console.log(`Migrated ${migratedCount} users to personal entities`);
213
+ }
214
+ /**
215
+ * Populate entity_id for existing projects.
216
+ */
217
+ async function populateProjectEntityIds(client, prefix) {
218
+ console.log('Populating entity_id for existing projects...');
219
+ // Update projects to use owner's personal entity
220
+ const result = await client.unsafe(`
221
+ UPDATE ${prefix}projects p
222
+ SET entity_id = e.id
223
+ FROM ${prefix}entities e
224
+ WHERE p.user_id = e.owner_user_id
225
+ AND e.entity_type = 'personal'
226
+ AND p.entity_id IS NULL
227
+ `);
228
+ console.log(`Updated entity_id for ${result.count || 0} projects`);
229
+ }
230
+ /**
231
+ * Generate a slug from a source string or random.
232
+ */
233
+ function generateSlug(source) {
234
+ if (source) {
235
+ // Normalize: lowercase, remove non-alphanumeric, take first 8 chars
236
+ return source
237
+ .toLowerCase()
238
+ .replace(/[^a-z0-9]/g, '')
239
+ .substring(0, 8)
240
+ .padEnd(8, '0');
241
+ }
242
+ // Generate random slug
243
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
244
+ let slug = '';
245
+ for (let i = 0; i < 8; i++) {
246
+ slug += chars.charAt(Math.floor(Math.random() * chars.length));
247
+ }
248
+ return slug;
249
+ }
250
+ /**
251
+ * Rollback the entity migration.
252
+ */
253
+ async function rollbackEntityMigration(config) {
254
+ const { client, schemaName, migrateProjects = true } = config;
255
+ const prefix = `${schemaName}.`;
256
+ console.log(`Rolling back entity migration for schema: ${schemaName}`);
257
+ // Remove entity_id from projects first
258
+ if (migrateProjects) {
259
+ await client.unsafe(`
260
+ ALTER TABLE ${prefix}projects DROP COLUMN IF EXISTS entity_id
261
+ `);
262
+ }
263
+ // Drop tables in reverse order
264
+ await client.unsafe(`DROP TABLE IF EXISTS ${prefix}entity_invitations`);
265
+ await client.unsafe(`DROP TABLE IF EXISTS ${prefix}entity_members`);
266
+ await client.unsafe(`DROP TABLE IF EXISTS ${prefix}entities`);
267
+ console.log('Entity migration rolled back');
268
+ }
269
+ //# sourceMappingURL=001_add_entities.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @fileoverview Entity Tables Migration
3
+ * @description Creates entity tables and migrates existing data
4
+ *
5
+ * This migration:
6
+ * 1. Creates entities, entity_members, entity_invitations tables
7
+ * 2. Adds entity_id column to projects table (nullable for backward compatibility)
8
+ * 3. Creates personal entities for existing users
9
+ * 4. Populates entity_id for existing projects
10
+ */
11
+ export interface MigrationConfig {
12
+ /** postgres-js client instance */
13
+ client: ReturnType<typeof import('postgres')>;
14
+ /** PostgreSQL schema name (e.g., 'whisperly', 'shapeshyft') */
15
+ schemaName: string;
16
+ /** Index prefix for avoiding name conflicts */
17
+ indexPrefix: string;
18
+ /** Whether to also add entity_id to projects table */
19
+ migrateProjects?: boolean;
20
+ }
21
+ /**
22
+ * Run the full entity migration.
23
+ */
24
+ export declare function runEntityMigration(config: MigrationConfig): Promise<void>;
25
+ /**
26
+ * Rollback the entity migration.
27
+ */
28
+ export declare function rollbackEntityMigration(config: MigrationConfig): Promise<void>;
29
+ //# sourceMappingURL=001_add_entities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"001_add_entities.d.ts","sourceRoot":"","sources":["../../src/migrations/001_add_entities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,MAAM,WAAW,eAAe;IAC9B,kCAAkC;IAClC,MAAM,EAAE,UAAU,CAAC,cAAc,UAAU,CAAC,CAAC,CAAC;IAC9C,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB/E;AAqQD;;GAEG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBpF"}
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Entity Tables Migration
4
+ * @description Creates entity tables and migrates existing data
5
+ *
6
+ * This migration:
7
+ * 1. Creates entities, entity_members, entity_invitations tables
8
+ * 2. Adds entity_id column to projects table (nullable for backward compatibility)
9
+ * 3. Creates personal entities for existing users
10
+ * 4. Populates entity_id for existing projects
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.runEntityMigration = runEntityMigration;
14
+ exports.rollbackEntityMigration = rollbackEntityMigration;
15
+ /**
16
+ * Run the full entity migration.
17
+ */
18
+ async function runEntityMigration(config) {
19
+ const { client, schemaName, indexPrefix, migrateProjects = true } = config;
20
+ const prefix = `${schemaName}.`;
21
+ console.log(`Running entity migration for schema: ${schemaName}`);
22
+ // Step 1: Create entity tables
23
+ await createEntityTables(client, prefix, indexPrefix);
24
+ // Step 2: Add entity_id to projects if requested
25
+ if (migrateProjects) {
26
+ await addEntityIdToProjects(client, prefix, indexPrefix);
27
+ }
28
+ // Step 3: Migrate existing users to personal entities
29
+ await migrateUsersToPersonalEntities(client, prefix);
30
+ // Step 4: Populate entity_id for existing projects
31
+ if (migrateProjects) {
32
+ await populateProjectEntityIds(client, prefix);
33
+ }
34
+ console.log('Entity migration completed successfully');
35
+ }
36
+ /**
37
+ * Create the entity tables.
38
+ */
39
+ async function createEntityTables(client, prefix, indexPrefix) {
40
+ console.log('Creating entity tables...');
41
+ // Create entities table
42
+ await client.unsafe(`
43
+ CREATE TABLE IF NOT EXISTS ${prefix}entities (
44
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
45
+ entity_slug VARCHAR(12) NOT NULL UNIQUE,
46
+ entity_type VARCHAR(20) NOT NULL CHECK (entity_type IN ('personal', 'organization')),
47
+ display_name VARCHAR(255) NOT NULL,
48
+ description TEXT,
49
+ avatar_url TEXT,
50
+ owner_user_id UUID NOT NULL,
51
+ created_at TIMESTAMPTZ DEFAULT NOW(),
52
+ updated_at TIMESTAMPTZ DEFAULT NOW()
53
+ )
54
+ `);
55
+ await client.unsafe(`
56
+ CREATE UNIQUE INDEX IF NOT EXISTS ${indexPrefix}_entities_slug_idx
57
+ ON ${prefix}entities (entity_slug)
58
+ `);
59
+ await client.unsafe(`
60
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entities_owner_idx
61
+ ON ${prefix}entities (owner_user_id)
62
+ `);
63
+ await client.unsafe(`
64
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entities_type_idx
65
+ ON ${prefix}entities (entity_type)
66
+ `);
67
+ // Create entity_members table
68
+ await client.unsafe(`
69
+ CREATE TABLE IF NOT EXISTS ${prefix}entity_members (
70
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
71
+ entity_id UUID NOT NULL REFERENCES ${prefix}entities(id) ON DELETE CASCADE,
72
+ user_id UUID NOT NULL,
73
+ role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'manager', 'viewer')),
74
+ joined_at TIMESTAMPTZ DEFAULT NOW(),
75
+ created_at TIMESTAMPTZ DEFAULT NOW(),
76
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
77
+ UNIQUE(entity_id, user_id)
78
+ )
79
+ `);
80
+ await client.unsafe(`
81
+ CREATE UNIQUE INDEX IF NOT EXISTS ${indexPrefix}_entity_members_entity_user_idx
82
+ ON ${prefix}entity_members (entity_id, user_id)
83
+ `);
84
+ await client.unsafe(`
85
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entity_members_entity_idx
86
+ ON ${prefix}entity_members (entity_id)
87
+ `);
88
+ await client.unsafe(`
89
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entity_members_user_idx
90
+ ON ${prefix}entity_members (user_id)
91
+ `);
92
+ // Create entity_invitations table
93
+ await client.unsafe(`
94
+ CREATE TABLE IF NOT EXISTS ${prefix}entity_invitations (
95
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
96
+ entity_id UUID NOT NULL REFERENCES ${prefix}entities(id) ON DELETE CASCADE,
97
+ email VARCHAR(255) NOT NULL,
98
+ role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'manager', 'viewer')),
99
+ status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined', 'expired')),
100
+ invited_by_user_id UUID NOT NULL,
101
+ token VARCHAR(64) NOT NULL UNIQUE,
102
+ expires_at TIMESTAMPTZ NOT NULL,
103
+ accepted_at TIMESTAMPTZ,
104
+ created_at TIMESTAMPTZ DEFAULT NOW(),
105
+ updated_at TIMESTAMPTZ DEFAULT NOW()
106
+ )
107
+ `);
108
+ await client.unsafe(`
109
+ CREATE UNIQUE INDEX IF NOT EXISTS ${indexPrefix}_entity_invitations_token_idx
110
+ ON ${prefix}entity_invitations (token)
111
+ `);
112
+ await client.unsafe(`
113
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entity_invitations_entity_idx
114
+ ON ${prefix}entity_invitations (entity_id)
115
+ `);
116
+ await client.unsafe(`
117
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entity_invitations_email_idx
118
+ ON ${prefix}entity_invitations (email)
119
+ `);
120
+ await client.unsafe(`
121
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_entity_invitations_status_idx
122
+ ON ${prefix}entity_invitations (status)
123
+ `);
124
+ console.log('Entity tables created');
125
+ }
126
+ /**
127
+ * Add entity_id column to projects table.
128
+ */
129
+ async function addEntityIdToProjects(client, prefix, indexPrefix) {
130
+ console.log('Adding entity_id to projects table...');
131
+ // Check if column already exists
132
+ const columnExists = await client.unsafe(`
133
+ SELECT EXISTS (
134
+ SELECT 1 FROM information_schema.columns
135
+ WHERE table_schema = '${prefix.replace('.', '')}'
136
+ AND table_name = 'projects'
137
+ AND column_name = 'entity_id'
138
+ )
139
+ `);
140
+ if (!columnExists[0]?.exists) {
141
+ // Add nullable entity_id column
142
+ await client.unsafe(`
143
+ ALTER TABLE ${prefix}projects
144
+ ADD COLUMN entity_id UUID REFERENCES ${prefix}entities(id) ON DELETE CASCADE
145
+ `);
146
+ // Create index on entity_id
147
+ await client.unsafe(`
148
+ CREATE INDEX IF NOT EXISTS ${indexPrefix}_projects_entity_idx
149
+ ON ${prefix}projects (entity_id)
150
+ `);
151
+ console.log('entity_id column added to projects');
152
+ }
153
+ else {
154
+ console.log('entity_id column already exists in projects');
155
+ }
156
+ }
157
+ /**
158
+ * Create personal entities for existing users.
159
+ */
160
+ async function migrateUsersToPersonalEntities(client, prefix) {
161
+ console.log('Migrating users to personal entities...');
162
+ // Get users without personal entities
163
+ const usersWithoutEntities = await client.unsafe(`
164
+ SELECT u.id, u.email, u.display_name,
165
+ COALESCE(s.organization_path, SUBSTRING(MD5(RANDOM()::TEXT) FROM 1 FOR 8)) as slug_source
166
+ FROM ${prefix}users u
167
+ LEFT JOIN ${prefix}user_settings s ON u.id = s.user_id
168
+ WHERE NOT EXISTS (
169
+ SELECT 1 FROM ${prefix}entities e
170
+ WHERE e.owner_user_id = u.id AND e.entity_type = 'personal'
171
+ )
172
+ `);
173
+ let migratedCount = 0;
174
+ for (const user of usersWithoutEntities) {
175
+ // Generate a unique slug (8 chars, lowercase alphanumeric)
176
+ const slug = generateSlug(user.slug_source);
177
+ const displayName = user.display_name || user.email?.split('@')[0] || 'Personal';
178
+ try {
179
+ // Create personal entity
180
+ const [entity] = await client.unsafe(`
181
+ INSERT INTO ${prefix}entities (entity_slug, entity_type, display_name, owner_user_id)
182
+ VALUES ('${slug}', 'personal', '${displayName.replace(/'/g, "''")}', '${user.id}')
183
+ RETURNING id
184
+ `);
185
+ // Add user as admin member
186
+ await client.unsafe(`
187
+ INSERT INTO ${prefix}entity_members (entity_id, user_id, role)
188
+ VALUES ('${entity.id}', '${user.id}', 'admin')
189
+ `);
190
+ migratedCount++;
191
+ }
192
+ catch (error) {
193
+ // If slug collision, generate a new one and retry
194
+ if (error.code === '23505') {
195
+ const newSlug = generateSlug();
196
+ const [entity] = await client.unsafe(`
197
+ INSERT INTO ${prefix}entities (entity_slug, entity_type, display_name, owner_user_id)
198
+ VALUES ('${newSlug}', 'personal', '${displayName.replace(/'/g, "''")}', '${user.id}')
199
+ RETURNING id
200
+ `);
201
+ await client.unsafe(`
202
+ INSERT INTO ${prefix}entity_members (entity_id, user_id, role)
203
+ VALUES ('${entity.id}', '${user.id}', 'admin')
204
+ `);
205
+ migratedCount++;
206
+ }
207
+ else {
208
+ throw error;
209
+ }
210
+ }
211
+ }
212
+ console.log(`Migrated ${migratedCount} users to personal entities`);
213
+ }
214
+ /**
215
+ * Populate entity_id for existing projects.
216
+ */
217
+ async function populateProjectEntityIds(client, prefix) {
218
+ console.log('Populating entity_id for existing projects...');
219
+ // Update projects to use owner's personal entity
220
+ const result = await client.unsafe(`
221
+ UPDATE ${prefix}projects p
222
+ SET entity_id = e.id
223
+ FROM ${prefix}entities e
224
+ WHERE p.user_id = e.owner_user_id
225
+ AND e.entity_type = 'personal'
226
+ AND p.entity_id IS NULL
227
+ `);
228
+ console.log(`Updated entity_id for ${result.count || 0} projects`);
229
+ }
230
+ /**
231
+ * Generate a slug from a source string or random.
232
+ */
233
+ function generateSlug(source) {
234
+ if (source) {
235
+ // Normalize: lowercase, remove non-alphanumeric, take first 8 chars
236
+ return source
237
+ .toLowerCase()
238
+ .replace(/[^a-z0-9]/g, '')
239
+ .substring(0, 8)
240
+ .padEnd(8, '0');
241
+ }
242
+ // Generate random slug
243
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
244
+ let slug = '';
245
+ for (let i = 0; i < 8; i++) {
246
+ slug += chars.charAt(Math.floor(Math.random() * chars.length));
247
+ }
248
+ return slug;
249
+ }
250
+ /**
251
+ * Rollback the entity migration.
252
+ */
253
+ async function rollbackEntityMigration(config) {
254
+ const { client, schemaName, migrateProjects = true } = config;
255
+ const prefix = `${schemaName}.`;
256
+ console.log(`Rolling back entity migration for schema: ${schemaName}`);
257
+ // Remove entity_id from projects first
258
+ if (migrateProjects) {
259
+ await client.unsafe(`
260
+ ALTER TABLE ${prefix}projects DROP COLUMN IF EXISTS entity_id
261
+ `);
262
+ }
263
+ // Drop tables in reverse order
264
+ await client.unsafe(`DROP TABLE IF EXISTS ${prefix}entity_invitations`);
265
+ await client.unsafe(`DROP TABLE IF EXISTS ${prefix}entity_members`);
266
+ await client.unsafe(`DROP TABLE IF EXISTS ${prefix}entities`);
267
+ console.log('Entity migration rolled back');
268
+ }
269
+ //# sourceMappingURL=001_add_entities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"001_add_entities.js","sourceRoot":"","sources":["../../src/migrations/001_add_entities.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Migration Exports
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.rollbackEntityMigration = exports.runEntityMigration = void 0;
7
+ var _001_add_entities_1 = require("./001_add_entities");
8
+ Object.defineProperty(exports, "runEntityMigration", { enumerable: true, get: function () { return _001_add_entities_1.runEntityMigration; } });
9
+ Object.defineProperty(exports, "rollbackEntityMigration", { enumerable: true, get: function () { return _001_add_entities_1.rollbackEntityMigration; } });
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Migration Exports
3
+ */
4
+ export { runEntityMigration, rollbackEntityMigration, type MigrationConfig, } from './001_add_entities';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Migration Exports
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.rollbackEntityMigration = exports.runEntityMigration = void 0;
7
+ var _001_add_entities_1 = require("./001_add_entities");
8
+ Object.defineProperty(exports, "runEntityMigration", { enumerable: true, get: function () { return _001_add_entities_1.runEntityMigration; } });
9
+ Object.defineProperty(exports, "rollbackEntityMigration", { enumerable: true, get: function () { return _001_add_entities_1.rollbackEntityMigration; } });
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AAEH,wDAI4B;AAH1B,uHAAA,kBAAkB,OAAA;AAClB,4HAAA,uBAAuB,OAAA"}