@jskit-ai/users-core 0.1.33 → 0.1.36

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.
@@ -1,10 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { createWorkspaceBootstrapContributor } from "../src/server/workspaceBootstrapContributor.js";
4
- import {
5
- TENANCY_MODE_PERSONAL,
6
- WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
7
- } from "../src/shared/tenancyProfile.js";
8
4
 
9
5
  function createAuthenticatedProfile(overrides = {}) {
10
6
  return {
@@ -40,26 +36,10 @@ test("workspace bootstrap contributor passes actor context to pending invites se
40
36
  }
41
37
  },
42
38
  usersRepository: {
43
- async findByIdentity() {
39
+ async findById() {
44
40
  return profile;
45
41
  }
46
42
  },
47
- userSettingsRepository: {
48
- async ensureForUserId() {
49
- return {
50
- theme: "system",
51
- locale: "en",
52
- timeZone: "UTC",
53
- dateFormat: "YYYY-MM-DD",
54
- numberFormat: "1,234.56",
55
- currencyCode: "USD",
56
- avatarSize: 64,
57
- productUpdates: true,
58
- accountActivity: true,
59
- securityAlerts: true
60
- };
61
- }
62
- },
63
43
  workspaceInvitationsEnabled: true,
64
44
  appConfig: {
65
45
  tenancyMode: "workspaces"
@@ -67,15 +47,12 @@ test("workspace bootstrap contributor passes actor context to pending invites se
67
47
  });
68
48
 
69
49
  await contributor.contribute({
70
- request: {
71
- async executeAction() {
72
- return {
73
- authenticated: true,
74
- profile
75
- };
50
+ payload: {
51
+ session: {
52
+ authenticated: true,
53
+ userId: profile.id
76
54
  }
77
- },
78
- reply: {}
55
+ }
79
56
  });
80
57
 
81
58
  assert.equal(pendingServiceCalls.length, 1);
@@ -83,133 +60,6 @@ test("workspace bootstrap contributor passes actor context to pending invites se
83
60
  assert.equal(pendingServiceCalls[0].options?.context?.actor?.id, profile.id);
84
61
  });
85
62
 
86
- test("workspace bootstrap contributor seeds the initial console owner on authenticated bootstrap", async () => {
87
- const profile = createAuthenticatedProfile({ id: 12 });
88
- const consoleOwnerSeeds = [];
89
-
90
- const contributor = createWorkspaceBootstrapContributor({
91
- workspaceService: {
92
- async listWorkspacesForUser() {
93
- return [];
94
- },
95
- async resolveWorkspaceContextForUserBySlug() {
96
- return null;
97
- }
98
- },
99
- workspacePendingInvitationsService: {
100
- async listPendingInvitesForUser() {
101
- return [];
102
- }
103
- },
104
- usersRepository: {
105
- async findByIdentity() {
106
- return profile;
107
- }
108
- },
109
- userSettingsRepository: {
110
- async ensureForUserId() {
111
- return {
112
- theme: "system",
113
- locale: "en",
114
- timeZone: "UTC",
115
- dateFormat: "YYYY-MM-DD",
116
- numberFormat: "1,234.56",
117
- currencyCode: "USD",
118
- avatarSize: 64,
119
- productUpdates: true,
120
- accountActivity: true,
121
- securityAlerts: true
122
- };
123
- }
124
- },
125
- workspaceInvitationsEnabled: false,
126
- consoleService: {
127
- async ensureInitialConsoleMember(userId) {
128
- consoleOwnerSeeds.push(Number(userId));
129
- return Number(userId);
130
- }
131
- }
132
- });
133
-
134
- const payload = await contributor.contribute({
135
- request: {
136
- async executeAction() {
137
- return {
138
- authenticated: true,
139
- profile
140
- };
141
- }
142
- },
143
- reply: {}
144
- });
145
-
146
- assert.deepEqual(consoleOwnerSeeds, [12]);
147
- assert.equal(payload.surfaceAccess?.consoleowner, true);
148
- });
149
-
150
- test("workspace bootstrap contributor emits canonical tenancy profile from users-core", async () => {
151
- const contributor = createWorkspaceBootstrapContributor({
152
- workspaceService: {
153
- async listWorkspacesForUser() {
154
- return [];
155
- },
156
- async resolveWorkspaceContextForUserBySlug() {
157
- return null;
158
- }
159
- },
160
- workspacePendingInvitationsService: {
161
- async listPendingInvitesForUser() {
162
- return [];
163
- }
164
- },
165
- usersRepository: {
166
- async findByIdentity() {
167
- return null;
168
- }
169
- },
170
- userSettingsRepository: {
171
- async ensureForUserId() {
172
- return {};
173
- }
174
- },
175
- workspaceInvitationsEnabled: false,
176
- tenancyProfile: {
177
- mode: TENANCY_MODE_PERSONAL,
178
- workspace: {
179
- enabled: true,
180
- autoProvision: true,
181
- allowSelfCreate: false,
182
- slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
183
- }
184
- },
185
- appConfig: {
186
- tenancyMode: "none"
187
- }
188
- });
189
-
190
- const payload = await contributor.contribute({
191
- request: {
192
- async executeAction() {
193
- return {
194
- authenticated: false
195
- };
196
- }
197
- },
198
- reply: {}
199
- });
200
-
201
- assert.deepEqual(payload.tenancy, {
202
- mode: TENANCY_MODE_PERSONAL,
203
- workspace: {
204
- enabled: true,
205
- autoProvision: true,
206
- allowSelfCreate: false,
207
- slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
208
- }
209
- });
210
- assert.equal(payload.app.tenancyMode, undefined);
211
- });
212
-
213
63
  test("workspace bootstrap contributor resolves workspace slug from bootstrap query", async () => {
214
64
  const profile = createAuthenticatedProfile();
215
65
  const calls = [];
@@ -229,162 +79,56 @@ test("workspace bootstrap contributor resolves workspace slug from bootstrap que
229
79
  }
230
80
  },
231
81
  usersRepository: {
232
- async findByIdentity() {
82
+ async findById() {
233
83
  return profile;
234
84
  }
235
85
  },
236
- userSettingsRepository: {
237
- async ensureForUserId() {
238
- return {
239
- theme: "system",
240
- locale: "en",
241
- timeZone: "UTC",
242
- dateFormat: "YYYY-MM-DD",
243
- numberFormat: "1,234.56",
244
- currencyCode: "USD",
245
- avatarSize: 64,
246
- productUpdates: true,
247
- accountActivity: true,
248
- securityAlerts: true
249
- };
250
- }
251
- },
252
86
  workspaceInvitationsEnabled: true,
253
87
  appConfig: {
254
88
  tenancyMode: "workspaces"
255
89
  }
256
90
  });
257
91
 
258
- await contributor.contribute({
92
+ const payload = await contributor.contribute({
259
93
  query: {
260
94
  workspaceSlug: " AcMe "
261
95
  },
262
- request: {
263
- async executeAction() {
264
- return {
265
- authenticated: true,
266
- profile
267
- };
96
+ payload: {
97
+ session: {
98
+ authenticated: true,
99
+ userId: profile.id
268
100
  }
269
- },
270
- reply: {}
271
- });
272
-
273
- assert.deepEqual(calls, ["acme"]);
274
- });
275
-
276
- test("workspace bootstrap contributor returns global payload with requestedWorkspace=forbidden when slug access is denied", async () => {
277
- const profile = createAuthenticatedProfile();
278
- const contributor = createWorkspaceBootstrapContributor({
279
- workspaceService: {
280
- async listWorkspacesForUser() {
281
- return [{ id: 3, slug: "chiara", name: "Chiara Workspace" }];
282
- },
283
- async resolveWorkspaceContextForUserBySlug() {
284
- const error = new Error("Forbidden.");
285
- error.status = 403;
286
- throw error;
287
- }
288
- },
289
- workspacePendingInvitationsService: {
290
- async listPendingInvitesForUser() {
291
- return [];
292
- }
293
- },
294
- usersRepository: {
295
- async findByIdentity() {
296
- return profile;
297
- }
298
- },
299
- userSettingsRepository: {
300
- async ensureForUserId() {
301
- return {
302
- theme: "system",
303
- locale: "en",
304
- timeZone: "UTC",
305
- dateFormat: "YYYY-MM-DD",
306
- numberFormat: "1,234.56",
307
- currencyCode: "USD",
308
- avatarSize: 64,
309
- productUpdates: true,
310
- accountActivity: true,
311
- securityAlerts: true
312
- };
313
- }
314
- },
315
- workspaceInvitationsEnabled: true,
316
- appConfig: {
317
- tenancyMode: "workspaces"
318
101
  }
319
102
  });
320
103
 
321
- const payload = await contributor.contribute({
322
- query: {
323
- workspaceSlug: "tonymobily"
324
- },
325
- request: {
326
- async executeAction() {
327
- return {
328
- authenticated: true,
329
- profile
330
- };
331
- }
332
- },
333
- reply: {}
334
- });
335
-
336
- assert.equal(payload.session.authenticated, true);
337
- assert.deepEqual(payload.workspaces, [{ id: 3, slug: "chiara", name: "Chiara Workspace" }]);
104
+ assert.deepEqual(calls, ["acme"]);
338
105
  assert.deepEqual(payload.requestedWorkspace, {
339
- slug: "tonymobily",
340
- status: "forbidden"
106
+ slug: "acme",
107
+ status: "resolved"
341
108
  });
342
- assert.equal(payload.activeWorkspace, null);
343
- assert.equal(payload.membership, null);
344
- assert.deepEqual(payload.permissions, []);
345
- assert.equal(payload.workspaceSettings, null);
346
109
  });
347
110
 
348
- test("workspace bootstrap contributor returns requestedWorkspace=not_found when slug does not exist", async () => {
349
- const profile = createAuthenticatedProfile();
111
+ test("workspace bootstrap contributor reports unauthenticated requested workspace without generic bootstrap work", async () => {
350
112
  const contributor = createWorkspaceBootstrapContributor({
351
113
  workspaceService: {
352
114
  async listWorkspacesForUser() {
353
- return [{ id: 1, slug: "acme", name: "Acme Workspace" }];
115
+ assert.fail("listWorkspacesForUser should not run for unauthenticated payloads");
354
116
  },
355
117
  async resolveWorkspaceContextForUserBySlug() {
356
- const error = new Error("Workspace not found.");
357
- error.status = 404;
358
- throw error;
118
+ assert.fail("resolveWorkspaceContextForUserBySlug should not run for unauthenticated payloads");
359
119
  }
360
120
  },
361
121
  workspacePendingInvitationsService: {
362
122
  async listPendingInvitesForUser() {
363
- return [];
123
+ assert.fail("listPendingInvitesForUser should not run for unauthenticated payloads");
364
124
  }
365
125
  },
366
126
  usersRepository: {
367
- async findByIdentity() {
368
- return profile;
369
- }
370
- },
371
- userSettingsRepository: {
372
- async ensureForUserId() {
373
- return {
374
- theme: "system",
375
- locale: "en",
376
- timeZone: "UTC",
377
- dateFormat: "YYYY-MM-DD",
378
- numberFormat: "1,234.56",
379
- currencyCode: "USD",
380
- avatarSize: 64,
381
- productUpdates: true,
382
- accountActivity: true,
383
- securityAlerts: true
384
- };
127
+ async findById() {
128
+ assert.fail("findById should not run for unauthenticated payloads");
385
129
  }
386
130
  },
387
- workspaceInvitationsEnabled: false,
131
+ workspaceInvitationsEnabled: true,
388
132
  appConfig: {
389
133
  tenancyMode: "workspaces"
390
134
  }
@@ -392,75 +136,19 @@ test("workspace bootstrap contributor returns requestedWorkspace=not_found when
392
136
 
393
137
  const payload = await contributor.contribute({
394
138
  query: {
395
- workspaceSlug: "missing-workspace"
139
+ workspaceSlug: "AcMe"
396
140
  },
397
- request: {
398
- async executeAction() {
399
- return {
400
- authenticated: true,
401
- profile
402
- };
141
+ payload: {
142
+ session: {
143
+ authenticated: false
403
144
  }
404
- },
405
- reply: {}
406
- });
407
-
408
- assert.deepEqual(payload.requestedWorkspace, {
409
- slug: "missing-workspace",
410
- status: "not_found"
411
- });
412
- assert.deepEqual(payload.workspaces, [{ id: 1, slug: "acme", name: "Acme Workspace" }]);
413
- });
414
-
415
- test("workspace bootstrap contributor returns requestedWorkspace=unauthenticated for anonymous workspace slug query", async () => {
416
- const contributor = createWorkspaceBootstrapContributor({
417
- workspaceService: {
418
- async listWorkspacesForUser() {
419
- return [];
420
- },
421
- async resolveWorkspaceContextForUserBySlug() {
422
- return null;
423
- }
424
- },
425
- workspacePendingInvitationsService: {
426
- async listPendingInvitesForUser() {
427
- return [];
428
- }
429
- },
430
- usersRepository: {
431
- async findByIdentity() {
432
- return null;
433
- }
434
- },
435
- userSettingsRepository: {
436
- async ensureForUserId() {
437
- return {};
438
- }
439
- },
440
- workspaceInvitationsEnabled: false,
441
- appConfig: {
442
- tenancyMode: "workspaces"
443
145
  }
444
146
  });
445
147
 
446
- const payload = await contributor.contribute({
447
- query: {
448
- workspaceSlug: "tonymobily"
449
- },
450
- request: {
451
- async executeAction() {
452
- return {
453
- authenticated: false
454
- };
455
- }
456
- },
457
- reply: {}
458
- });
459
-
460
- assert.equal(payload.session.authenticated, false);
461
- assert.deepEqual(payload.requestedWorkspace, {
462
- slug: "tonymobily",
463
- status: "unauthenticated"
148
+ assert.deepEqual(payload, {
149
+ requestedWorkspace: {
150
+ slug: "acme",
151
+ status: "unauthenticated"
152
+ }
464
153
  });
465
- assert.deepEqual(payload.workspaces, []);
466
154
  });
@@ -1,3 +1,3 @@
1
- import "../templates/packages/main/src/shared/resources/workspaceSettingsFields.js";
1
+ import "../../workspaces-core/templates/packages/main/src/shared/resources/workspaceSettingsFields.js";
2
2
  import "../templates/packages/main/src/shared/resources/consoleSettingsFields.js";
3
3
  import "../templates/packages/main/src/shared/resources/userSettingsFields.js";
@@ -1,27 +0,0 @@
1
- export const roleCatalog = {
2
- workspace: {
3
- defaultInviteRole: "member"
4
- },
5
- roles: {
6
- owner: {
7
- assignable: false,
8
- permissions: ["*"]
9
- },
10
- admin: {
11
- assignable: true,
12
- inherits: "member",
13
- permissions: [
14
- "workspace.roles.view",
15
- "workspace.settings.update",
16
- "workspace.members.view",
17
- "workspace.members.invite",
18
- "workspace.members.manage",
19
- "workspace.invites.revoke"
20
- ]
21
- },
22
- member: {
23
- assignable: true,
24
- permissions: ["workspace.settings.view"]
25
- }
26
- }
27
- };
@@ -1,123 +0,0 @@
1
- /**
2
- * @param {import('knex').Knex} knex
3
- */
4
- exports.up = async function up(knex) {
5
- await knex.schema.createTable("users", (table) => {
6
- table.increments("id").primary();
7
- table.string("auth_provider", 64).notNullable();
8
- table.string("auth_provider_user_sid", 191).notNullable();
9
- table.string("email", 255).notNullable();
10
- table.string("username", 120).notNullable();
11
- table.string("display_name", 160).notNullable();
12
- table.string("avatar_storage_key", 512).nullable();
13
- table.string("avatar_version", 64).nullable();
14
- table.timestamp("avatar_updated_at", { useTz: false }).nullable();
15
- table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
16
- table.unique(["auth_provider", "auth_provider_user_sid"], "uq_users_identity");
17
- table.unique(["email"], "uq_users_email");
18
- table.unique(["username"], "uq_users_username");
19
- });
20
-
21
- await knex.schema.createTable("workspaces", (table) => {
22
- table.increments("id").primary();
23
- table.string("slug", 120).notNullable().unique();
24
- table.string("name", 160).notNullable();
25
- table.integer("owner_user_id").unsigned().notNullable().references("id").inTable("users").onDelete("CASCADE");
26
- table.boolean("is_personal").notNullable().defaultTo(true);
27
- table.string("avatar_url", 512).notNullable().defaultTo("");
28
- table.string("color", 7).notNullable().defaultTo("#1867C0");
29
- table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
30
- table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
31
- table.timestamp("deleted_at", { useTz: false }).nullable();
32
- });
33
-
34
- await knex.schema.createTable("workspace_memberships", (table) => {
35
- table.increments("id").primary();
36
- table.integer("workspace_id").unsigned().notNullable().references("id").inTable("workspaces").onDelete("CASCADE");
37
- table.integer("user_id").unsigned().notNullable().references("id").inTable("users").onDelete("CASCADE");
38
- table.string("role_sid", 64).notNullable().defaultTo("member");
39
- table.string("status", 32).notNullable().defaultTo("active");
40
- table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
41
- table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
42
- table.unique(["workspace_id", "user_id"], "uq_workspace_memberships_workspace_user");
43
- });
44
-
45
- await knex.schema.createTable("workspace_settings", (table) => {
46
- table.integer("workspace_id").unsigned().primary().references("id").inTable("workspaces").onDelete("CASCADE");
47
- table.string("name", 160).notNullable().defaultTo("Workspace");
48
- table.string("avatar_url", 512).notNullable().defaultTo("");
49
- table.string("light_primary_color", 7).notNullable().defaultTo("#1867C0");
50
- table.string("light_secondary_color", 7).notNullable().defaultTo("#48A9A6");
51
- table.string("light_surface_color", 7).notNullable().defaultTo("#FFFFFF");
52
- table.string("light_surface_variant_color", 7).notNullable().defaultTo("#424242");
53
- table.string("dark_primary_color", 7).notNullable().defaultTo("#2196F3");
54
- table.string("dark_secondary_color", 7).notNullable().defaultTo("#54B6B2");
55
- table.string("dark_surface_color", 7).notNullable().defaultTo("#212121");
56
- table.string("dark_surface_variant_color", 7).notNullable().defaultTo("#C8C8C8");
57
- table.boolean("invites_enabled").notNullable().defaultTo(true);
58
- table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
59
- table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
60
- });
61
-
62
- await knex.schema.createTable("workspace_invites", (table) => {
63
- table.increments("id").primary();
64
- table.integer("workspace_id").unsigned().notNullable().references("id").inTable("workspaces").onDelete("CASCADE");
65
- table.string("email", 255).notNullable();
66
- table.string("role_sid", 64).notNullable().defaultTo("member");
67
- table.string("status", 32).notNullable().defaultTo("pending");
68
- table.string("token_hash", 191).notNullable();
69
- table.integer("invited_by_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
70
- table.timestamp("expires_at", { useTz: false }).nullable();
71
- table.timestamp("accepted_at", { useTz: false }).nullable();
72
- table.timestamp("revoked_at", { useTz: false }).nullable();
73
- table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
74
- table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
75
- table.unique(["token_hash"], "uq_workspace_invites_token_hash");
76
- table.index(["workspace_id", "status"], "idx_workspace_invites_workspace_status");
77
- });
78
-
79
- await knex.schema.createTable("user_settings", (table) => {
80
- table.integer("user_id").unsigned().primary().references("id").inTable("users").onDelete("CASCADE");
81
- table.integer("last_active_workspace_id").unsigned().nullable().references("id").inTable("workspaces").onDelete("SET NULL");
82
- table.string("theme", 32).notNullable().defaultTo("system");
83
- table.string("locale", 24).notNullable().defaultTo("en");
84
- table.string("time_zone", 64).notNullable().defaultTo("UTC");
85
- table.string("date_format", 32).notNullable().defaultTo("yyyy-mm-dd");
86
- table.string("number_format", 32).notNullable().defaultTo("1,234.56");
87
- table.string("currency_code", 3).notNullable().defaultTo("USD");
88
- table.integer("avatar_size").notNullable().defaultTo(64);
89
- table.boolean("password_sign_in_enabled").notNullable().defaultTo(true);
90
- table.boolean("password_setup_required").notNullable().defaultTo(false);
91
- table.boolean("notify_product_updates").notNullable().defaultTo(true);
92
- table.boolean("notify_account_activity").notNullable().defaultTo(true);
93
- table.boolean("notify_security_alerts").notNullable().defaultTo(true);
94
- table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
95
- table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
96
- });
97
-
98
- await knex.schema.createTable("console_settings", (table) => {
99
- table.integer("id").primary();
100
- table.integer("owner_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
101
- table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
102
- table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
103
- });
104
-
105
- await knex("console_settings").insert({
106
- id: 1,
107
- created_at: knex.fn.now(),
108
- updated_at: knex.fn.now()
109
- });
110
- };
111
-
112
- /**
113
- * @param {import('knex').Knex} knex
114
- */
115
- exports.down = async function down(knex) {
116
- await knex.schema.dropTableIfExists("console_settings");
117
- await knex.schema.dropTableIfExists("user_settings");
118
- await knex.schema.dropTableIfExists("workspace_invites");
119
- await knex.schema.dropTableIfExists("workspace_settings");
120
- await knex.schema.dropTableIfExists("workspace_memberships");
121
- await knex.schema.dropTableIfExists("workspaces");
122
- await knex.schema.dropTableIfExists("users");
123
- };
@@ -1,71 +0,0 @@
1
- const WORKSPACE_SETTINGS_TABLE = "workspace_settings";
2
- const WORKSPACES_TABLE = "workspaces";
3
- const LEGACY_NAME_COLUMN = "name";
4
- const LEGACY_AVATAR_COLUMN = "avatar_url";
5
-
6
- async function hasTable(knex, tableName) {
7
- return knex.schema.hasTable(tableName);
8
- }
9
-
10
- async function hasColumn(knex, tableName, columnName) {
11
- return knex.schema.hasColumn(tableName, columnName);
12
- }
13
-
14
- exports.up = async function up(knex) {
15
- const hasWorkspaceSettings = await hasTable(knex, WORKSPACE_SETTINGS_TABLE);
16
- if (!hasWorkspaceSettings) {
17
- return;
18
- }
19
-
20
- const hasLegacyName = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_NAME_COLUMN);
21
- const hasLegacyAvatarUrl = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_AVATAR_COLUMN);
22
- if (!hasLegacyName && !hasLegacyAvatarUrl) {
23
- return;
24
- }
25
-
26
- await knex.schema.alterTable(WORKSPACE_SETTINGS_TABLE, (table) => {
27
- if (hasLegacyName) {
28
- table.dropColumn(LEGACY_NAME_COLUMN);
29
- }
30
- if (hasLegacyAvatarUrl) {
31
- table.dropColumn(LEGACY_AVATAR_COLUMN);
32
- }
33
- });
34
- };
35
-
36
- exports.down = async function down(knex) {
37
- const hasWorkspaceSettings = await hasTable(knex, WORKSPACE_SETTINGS_TABLE);
38
- if (!hasWorkspaceSettings) {
39
- return;
40
- }
41
-
42
- const hasLegacyName = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_NAME_COLUMN);
43
- const hasLegacyAvatarUrl = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_AVATAR_COLUMN);
44
- if (!hasLegacyName || !hasLegacyAvatarUrl) {
45
- await knex.schema.alterTable(WORKSPACE_SETTINGS_TABLE, (table) => {
46
- if (!hasLegacyName) {
47
- table.string(LEGACY_NAME_COLUMN, 160).notNullable().defaultTo("Workspace");
48
- }
49
- if (!hasLegacyAvatarUrl) {
50
- table.string(LEGACY_AVATAR_COLUMN, 512).notNullable().defaultTo("");
51
- }
52
- });
53
- }
54
-
55
- const hasWorkspaces = await hasTable(knex, WORKSPACES_TABLE);
56
- if (!hasWorkspaces) {
57
- return;
58
- }
59
-
60
- const workspaceRows = await knex(WORKSPACES_TABLE).select("id", "name", "avatar_url");
61
- for (const workspaceRow of workspaceRows) {
62
- const normalizedName = String(workspaceRow?.name || "").trim() || "Workspace";
63
- const normalizedAvatarUrl = String(workspaceRow?.avatar_url || "").trim();
64
- await knex(WORKSPACE_SETTINGS_TABLE)
65
- .where({ workspace_id: Number(workspaceRow.id) })
66
- .update({
67
- name: normalizedName,
68
- avatar_url: normalizedAvatarUrl
69
- });
70
- }
71
- };