@jskit-ai/workspaces-core 0.1.224 → 0.1.225

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/workspaces-core",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -18,11 +18,11 @@
18
18
  "./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js"
19
19
  },
20
20
  "dependencies": {
21
- "@jskit-ai/database-runtime": "0.1.246",
22
- "@jskit-ai/json-rest-api-core": "0.1.190",
23
- "@jskit-ai/resource-core": "0.1.188",
24
- "@jskit-ai/resource-crud-core": "0.1.188",
25
- "@jskit-ai/users-core": "0.1.259",
21
+ "@jskit-ai/database-runtime": "0.1.247",
22
+ "@jskit-ai/json-rest-api-core": "0.1.191",
23
+ "@jskit-ai/resource-core": "0.1.189",
24
+ "@jskit-ai/resource-crud-core": "0.1.189",
25
+ "@jskit-ai/users-core": "0.1.260",
26
26
  "json-rest-schema": "^1.0.17"
27
27
  },
28
28
  "description": "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
@@ -169,8 +169,8 @@
169
169
  }
170
170
  },
171
171
  "peerDependencies": {
172
- "@jskit-ai/auth-core": "0.1.244",
173
- "@jskit-ai/http-runtime": "0.1.244",
174
- "@jskit-ai/kernel": "0.1.246"
172
+ "@jskit-ai/auth-core": "0.1.245",
173
+ "@jskit-ai/http-runtime": "0.1.245",
174
+ "@jskit-ai/kernel": "0.1.247"
175
175
  }
176
176
  }
@@ -23,6 +23,12 @@ substitute for a simpler domain ownership column.
23
23
  Choose workspace creation policy, roles, invitation policy, expiry, email copy,
24
24
  member permissions, personal-workspace behavior, and surface access rules.
25
25
 
26
+ In `workspaces` mode, `tenancyPolicy.workspace.autoProvision: true` gives every
27
+ user a personal workspace in addition to shared ones; the server config function
28
+ `personalWorkspaceName(user)` names it. `workspaceSettings.defaults` sets
29
+ `invitesEnabled` and the colours (`lightPrimaryColor`, `darkPrimaryColor` and
30
+ the other palette fields) that new workspaces start with.
31
+
26
32
  ## Framework APIs
27
33
 
28
34
  Use workspace routes, resources, scope helpers, tenancy profiles, and settings
@@ -9,6 +9,7 @@ import { workspaceMembershipsResource } from "../shared/resources/workspaceMembe
9
9
  import { workspaceResource } from "../shared/resources/workspaceResource.js";
10
10
  import { workspaceSettingsResource } from "../shared/resources/workspaceSettingsResource.js";
11
11
  import { createWorkspaceRoleCatalog } from "../shared/roles.js";
12
+ import { WORKSPACE_SETTINGS_COLOR_FIELDS, normalizeWorkspaceHexColor } from "../shared/settings.js";
12
13
  import { resolveTenancyProfile } from "../shared/tenancyProfile.js";
13
14
  import { createRepository as createWorkspaceInvitesRepository } from "./common/repositories/workspaceInvitesRepository.js";
14
15
  import { createRepository as createWorkspaceMembershipsRepository } from "./common/repositories/workspaceMembershipsRepository.js";
@@ -49,6 +50,21 @@ function requireBoolean(value, label) {
49
50
  return value;
50
51
  }
51
52
 
53
+ function requireWorkspaceColorDefaults(defaults) {
54
+ const colors = {};
55
+ for (const fieldName of WORKSPACE_SETTINGS_COLOR_FIELDS) {
56
+ if (defaults?.[fieldName] === undefined) {
57
+ continue;
58
+ }
59
+ const color = normalizeWorkspaceHexColor(defaults[fieldName]);
60
+ if (!color) {
61
+ throw new TypeError(`runtime.config.workspaceSettings.defaults.${fieldName} must be a hex color like #1867C0.`);
62
+ }
63
+ colors[fieldName] = color;
64
+ }
65
+ return colors;
66
+ }
67
+
52
68
  function resolveInviteEmailTemplate(config) {
53
69
  const template =
54
70
  config.workspaceInviteEmailTemplate ||
@@ -92,7 +108,8 @@ function createWorkspacesRuntime({ config, env, jsonRestApi } = {}) {
92
108
  defaultInvitesEnabled: requireBoolean(
93
109
  config?.workspaceSettings?.defaults?.invitesEnabled,
94
110
  "runtime.config.workspaceSettings.defaults.invitesEnabled"
95
- )
111
+ ),
112
+ defaultColorSettings: requireWorkspaceColorDefaults(config?.workspaceSettings?.defaults)
96
113
  });
97
114
  const workspacesRepository = createWorkspacesRepository({ api: jsonRestApi });
98
115
  const workspaceMembershipsRepository = createWorkspaceMembershipsRepository({ api: jsonRestApi });
@@ -84,6 +84,8 @@ function createService({
84
84
  const resolvedTenancyProfile = resolveTenancyProfile(appConfig);
85
85
  const resolvedTenancyMode = resolvedTenancyProfile.mode;
86
86
  const workspacePolicy = resolvedTenancyProfile.workspace;
87
+ const personalWorkspaceName =
88
+ typeof appConfig.personalWorkspaceName === "function" ? appConfig.personalWorkspaceName : null;
87
89
  async function ensureUniqueWorkspaceSlug(baseSlug, options = {}) {
88
90
  let suffix = 0;
89
91
  while (suffix < 1000) {
@@ -121,7 +123,7 @@ function createService({
121
123
  const inserted = await workspacesRepository.insert(
122
124
  {
123
125
  slug,
124
- name: buildWorkspaceName(user),
126
+ name: normalizeText(personalWorkspaceName?.(user)) || buildWorkspaceName(user),
125
127
  ownerUserId: normalizedUserId,
126
128
  isPersonal: true,
127
129
  avatarUrl: ""
@@ -6,34 +6,12 @@ import {
6
6
  createJsonRestContext,
7
7
  extractJsonRestCollectionRows
8
8
  } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
9
+ import { pickOwnProperties } from "@jskit-ai/kernel/shared/support";
10
+ import { WORKSPACE_SETTINGS_FIELD_KEYS } from "../../shared/resources/workspaceSettingsResource.js";
9
11
  import { resolveWorkspaceThemePalettes } from "../../shared/settings.js";
10
12
 
11
- const WORKSPACE_SETTINGS_PATCH_FIELDS = Object.freeze([
12
- "lightPrimaryColor",
13
- "lightSecondaryColor",
14
- "lightSurfaceColor",
15
- "lightSurfaceVariantColor",
16
- "darkPrimaryColor",
17
- "darkSecondaryColor",
18
- "darkSurfaceColor",
19
- "darkSurfaceVariantColor",
20
- "invitesEnabled"
21
- ]);
22
-
23
- function pickPatchFields(source = {}) {
24
- const patch = {};
25
-
26
- for (const fieldName of WORKSPACE_SETTINGS_PATCH_FIELDS) {
27
- if (Object.hasOwn(source, fieldName)) {
28
- patch[fieldName] = source[fieldName];
29
- }
30
- }
31
-
32
- return patch;
33
- }
34
-
35
- function createDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvitesEnabled) {
36
- const palettes = resolveWorkspaceThemePalettes({});
13
+ function createDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvitesEnabled, defaultColorSettings = {}) {
14
+ const palettes = resolveWorkspaceThemePalettes(defaultColorSettings);
37
15
 
38
16
  return {
39
17
  id: workspaceId,
@@ -49,7 +27,7 @@ function createDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvites
49
27
  };
50
28
  }
51
29
 
52
- function createRepository({ api, defaultInvitesEnabled = true } = {}) {
30
+ function createRepository({ api, defaultInvitesEnabled = true, defaultColorSettings = {} } = {}) {
53
31
  if (!api?.resources?.workspaceSettings) {
54
32
  throw new TypeError("workspaceSettingsRepository requires json-rest-api workspaceSettings resource.");
55
33
  }
@@ -95,7 +73,11 @@ function createRepository({ api, defaultInvitesEnabled = true } = {}) {
95
73
  try {
96
74
  await api.resources.workspaceSettings.post(
97
75
  {
98
- data: createDefaultWorkspaceSettingsCreatePayload(normalizedWorkspaceId, defaultInvitesEnabled === true),
76
+ data: createDefaultWorkspaceSettingsCreatePayload(
77
+ normalizedWorkspaceId,
78
+ defaultInvitesEnabled === true,
79
+ defaultColorSettings
80
+ ),
99
81
  format: "plain",
100
82
  returning: "full",
101
83
  transaction: options?.trx || null
@@ -119,7 +101,7 @@ function createRepository({ api, defaultInvitesEnabled = true } = {}) {
119
101
 
120
102
  await ensureForWorkspaceId(normalizedWorkspaceId, options);
121
103
  const source = patch && typeof patch === "object" && !Array.isArray(patch) ? patch : {};
122
- const updatePayload = pickPatchFields(source);
104
+ const updatePayload = pickOwnProperties(source, WORKSPACE_SETTINGS_FIELD_KEYS);
123
105
 
124
106
  if (Object.keys(updatePayload).length < 1) {
125
107
  return findByWorkspaceId(normalizedWorkspaceId, options);
@@ -3,19 +3,14 @@ import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
3
3
  import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
4
4
  import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
5
5
  import { workspaceRoleCatalogSchema } from "./workspaceRoleCatalogSchema.js";
6
- import { normalizeWorkspaceHexColor, DEFAULT_WORKSPACE_DARK_PALETTE, DEFAULT_WORKSPACE_LIGHT_PALETTE } from "../settings.js";
6
+ import {
7
+ normalizeWorkspaceHexColor,
8
+ DEFAULT_WORKSPACE_DARK_PALETTE,
9
+ DEFAULT_WORKSPACE_LIGHT_PALETTE,
10
+ WORKSPACE_SETTINGS_COLOR_FIELDS
11
+ } from "../settings.js";
7
12
 
8
- const WORKSPACE_SETTINGS_FIELD_KEYS = deepFreeze([
9
- "lightPrimaryColor",
10
- "lightSecondaryColor",
11
- "lightSurfaceColor",
12
- "lightSurfaceVariantColor",
13
- "darkPrimaryColor",
14
- "darkSecondaryColor",
15
- "darkSurfaceColor",
16
- "darkSurfaceVariantColor",
17
- "invitesEnabled"
18
- ]);
13
+ const WORKSPACE_SETTINGS_FIELD_KEYS = deepFreeze([...WORKSPACE_SETTINGS_COLOR_FIELDS, "invitesEnabled"]);
19
14
 
20
15
  const workspaceSettingsBodySchema = createSchema({
21
16
  lightPrimaryColor: {
@@ -18,6 +18,17 @@ const DEFAULT_WORKSPACE_DARK_PALETTE = Object.freeze({
18
18
 
19
19
  const DEFAULT_WORKSPACE_COLOR = DEFAULT_WORKSPACE_LIGHT_PALETTE.color;
20
20
 
21
+ const WORKSPACE_SETTINGS_COLOR_FIELDS = Object.freeze([
22
+ "lightPrimaryColor",
23
+ "lightSecondaryColor",
24
+ "lightSurfaceColor",
25
+ "lightSurfaceVariantColor",
26
+ "darkPrimaryColor",
27
+ "darkSecondaryColor",
28
+ "darkSurfaceColor",
29
+ "darkSurfaceVariantColor"
30
+ ]);
31
+
21
32
  function normalizeWorkspaceHexColor(value) {
22
33
  const normalized = String(value || "").trim();
23
34
  if (!HEX_COLOR_PATTERN.test(normalized)) {
@@ -113,6 +124,7 @@ export {
113
124
  normalizeWorkspaceThemeMode,
114
125
  resolveWorkspaceThemeDefaultPalette,
115
126
  resolveWorkspaceThemePalettes,
127
+ WORKSPACE_SETTINGS_COLOR_FIELDS,
116
128
  WORKSPACE_THEME_MODE_DARK,
117
129
  WORKSPACE_THEME_MODE_LIGHT,
118
130
  resolveWorkspaceThemePalette
@@ -15,7 +15,8 @@ function resolveWorkspacePolicyOverrides(appConfig = {}) {
15
15
  const workspacePolicy = isRecord(tenancyPolicy.workspace) ? tenancyPolicy.workspace : {};
16
16
 
17
17
  return Object.freeze({
18
- allowSelfCreate: typeof workspacePolicy.allowSelfCreate === "boolean" ? workspacePolicy.allowSelfCreate : null
18
+ allowSelfCreate: typeof workspacePolicy.allowSelfCreate === "boolean" ? workspacePolicy.allowSelfCreate : null,
19
+ autoProvision: typeof workspacePolicy.autoProvision === "boolean" ? workspacePolicy.autoProvision : null
19
20
  });
20
21
  }
21
22
 
@@ -40,7 +41,7 @@ function resolveWorkspacePolicy(mode, overrides = {}) {
40
41
 
41
42
  return Object.freeze({
42
43
  enabled: true,
43
- autoProvision: false,
44
+ autoProvision: overrides.autoProvision === true,
44
45
  allowSelfCreate: overrides.allowSelfCreate === true,
45
46
  slugPolicy: WORKSPACE_SLUG_POLICY_USER_SELECTED
46
47
  });
@@ -5,7 +5,7 @@ import { createActionProvider } from "@jskit-ai/kernel/server/actions";
5
5
  import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
6
6
  import { createBootstrapRuntime } from "@jskit-ai/kernel/server/runtime";
7
7
  import { createUsersExtensions } from "@jskit-ai/users-core/server/usersExtensions";
8
- import { WorkspacesFeature } from "../src/server/WorkspacesFeature.js";
8
+ import { WorkspacesFeature, createWorkspacesRuntime } from "../src/server/WorkspacesFeature.js";
9
9
  import { WorkspacesIntegrationsProvider } from "../src/server/WorkspacesIntegrationsProvider.js";
10
10
 
11
11
  function createJsonRestApiFixture() {
@@ -157,3 +157,15 @@ test("workspace realtime audiences are the actor's active memberships", async ()
157
157
  { kind: "workspace_user", workspaceId: "12", userId: "9" }
158
158
  ]);
159
159
  });
160
+
161
+ test("workspace settings color defaults must be hex colors", () => {
162
+ const config = runtimeConfig();
163
+ config.workspaceSettings.defaults.lightPrimaryColor = "charcoal";
164
+ const jsonRestApi = createJsonRestApiFixture();
165
+ jsonRestApi.resources.workspaceSettings = {};
166
+
167
+ assert.throws(
168
+ () => createWorkspacesRuntime({ config, env: {}, jsonRestApi }),
169
+ /runtime\.config\.workspaceSettings\.defaults\.lightPrimaryColor must be a hex color/
170
+ );
171
+ });
@@ -65,3 +65,19 @@ test("resolveTenancyProfile allows explicit workspace self-create policy overrid
65
65
  assert.equal(workspaceProfile.mode, TENANCY_MODE_WORKSPACES);
66
66
  assert.equal(workspaceProfile.workspace.allowSelfCreate, true);
67
67
  });
68
+
69
+ test("resolveTenancyProfile lets workspace mode opt into a personal workspace for every user", () => {
70
+ const tenancyPolicy = { workspace: { autoProvision: true } };
71
+ assert.equal(
72
+ resolveTenancyProfile({ tenancyMode: TENANCY_MODE_WORKSPACES, tenancyPolicy }).workspace.autoProvision,
73
+ true
74
+ );
75
+ assert.equal(resolveTenancyProfile({ tenancyMode: TENANCY_MODE_NONE, tenancyPolicy }).workspace.autoProvision, false);
76
+ assert.equal(
77
+ resolveTenancyProfile({
78
+ tenancyMode: TENANCY_MODE_PERSONAL,
79
+ tenancyPolicy: { workspace: { autoProvision: false } }
80
+ }).workspace.autoProvision,
81
+ true
82
+ );
83
+ });
@@ -27,6 +27,7 @@ function createWorkspaceServiceFixture({
27
27
  additionalWorkspaces = [],
28
28
  userWorkspaceRows = null,
29
29
  membershipResolver = null,
30
+ personalWorkspaceName = undefined,
30
31
  personalWorkspace = {
31
32
  id: "1",
32
33
  slug: "tonymobily3",
@@ -72,6 +73,7 @@ function createWorkspaceServiceFixture({
72
73
  appConfig: {
73
74
  tenancyMode,
74
75
  tenancyPolicy,
76
+ personalWorkspaceName,
75
77
  roleCatalog: roleCatalog && typeof roleCatalog === "object" ? { ...roleCatalog } : roleCatalog
76
78
  },
77
79
  workspacesRepository: {
@@ -278,6 +280,32 @@ test("workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser provisions
278
280
  assert.equal(calls.ensureOwnerMembership, 1);
279
281
  assert.equal(calls.ensureWorkspaceSettings, 1);
280
282
  assert.equal(insertedPayloads[0].isPersonal, true);
283
+ assert.equal(insertedPayloads[0].name, "Chiara's Workspace");
284
+ });
285
+
286
+ test("workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser provisions a named personal workspace when workspace tenancy opts in", async () => {
287
+ const { service, calls, insertedPayloads } = createWorkspaceServiceFixture({
288
+ tenancyMode: "workspaces",
289
+ tenancyPolicy: {
290
+ workspace: {
291
+ autoProvision: true
292
+ }
293
+ },
294
+ personalWorkspaceName: (user) => `${user.displayName}'s home`,
295
+ personalWorkspace: null
296
+ });
297
+
298
+ const workspace = await service.ensureProvisionedWorkspaceForAuthenticatedUser({
299
+ id: "7",
300
+ email: "chiaramobily@gmail.com",
301
+ displayName: "Chiara"
302
+ });
303
+
304
+ assert.equal(Number(workspace.ownerUserId), 7);
305
+ assert.equal(calls.insert, 1);
306
+ assert.equal(calls.ensureOwnerMembership, 1);
307
+ assert.equal(insertedPayloads[0].isPersonal, true);
308
+ assert.equal(insertedPayloads[0].name, "Chiara's home");
281
309
  });
282
310
 
283
311
  test("workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser reuses the existing personal workspace in personal tenancy", async () => {
@@ -181,6 +181,25 @@ test("workspaceSettingsRepository.ensureForWorkspaceId delegates defaults to the
181
181
  assert.equal(record.id, "5");
182
182
  });
183
183
 
184
+ test("workspaceSettingsRepository.ensureForWorkspaceId starts new workspaces from the app's default colors", async () => {
185
+ const { api, state, DEFAULT_WORKSPACE_THEME } = createWorkspaceSettingsApiStub();
186
+ state.row = null;
187
+ const repository = createRepository({
188
+ api,
189
+ defaultColorSettings: {
190
+ lightPrimaryColor: "#1C1C1C",
191
+ darkPrimaryColor: "#F5F5F5"
192
+ }
193
+ });
194
+
195
+ await repository.ensureForWorkspaceId("5");
196
+
197
+ assert.equal(state.postPayload.data.lightPrimaryColor, "#1C1C1C");
198
+ assert.equal(state.postPayload.data.darkPrimaryColor, "#F5F5F5");
199
+ assert.equal(state.postPayload.data.lightSecondaryColor, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
200
+ assert.equal(state.postPayload.data.darkSurfaceColor, DEFAULT_WORKSPACE_THEME.dark.surfaceColor);
201
+ });
202
+
184
203
  test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates workspace settings fields", async () => {
185
204
  const { api, state } = createWorkspaceSettingsApiStub();
186
205
  const repository = createRepository({