@jskit-ai/workspaces-core 0.1.31 → 0.1.33

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 (62) hide show
  1. package/package.descriptor.mjs +11 -22
  2. package/package.json +11 -9
  3. package/src/server/WorkspacesCoreServiceProvider.js +22 -2
  4. package/src/server/common/repositories/workspaceInvitesRepository.js +233 -78
  5. package/src/server/common/repositories/workspaceMembershipsRepository.js +177 -86
  6. package/src/server/common/repositories/workspacesRepository.js +179 -86
  7. package/src/server/common/services/workspaceContextService.js +28 -26
  8. package/src/server/common/validators/routeParamsValidator.js +36 -53
  9. package/src/server/registerWorkspaceCore.js +9 -10
  10. package/src/server/registerWorkspaceRepositories.js +7 -3
  11. package/src/server/support/workspaceServerScopeSupport.js +1 -1
  12. package/src/server/workspaceBootstrapContributor.js +5 -14
  13. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +54 -27
  14. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +30 -24
  15. package/src/server/workspaceMembers/bootWorkspaceMembers.js +70 -32
  16. package/src/server/workspaceMembers/workspaceMembersActions.js +61 -27
  17. package/src/server/workspaceMembers/workspaceMembersService.js +43 -7
  18. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +28 -13
  19. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +13 -15
  20. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +33 -10
  21. package/src/server/workspaceSettings/bootWorkspaceSettings.js +32 -13
  22. package/src/server/workspaceSettings/registerWorkspaceSettings.js +5 -1
  23. package/src/server/workspaceSettings/workspaceSettingsActions.js +18 -12
  24. package/src/server/workspaceSettings/workspaceSettingsRepository.js +104 -91
  25. package/src/server/workspaceSettings/workspaceSettingsService.js +5 -6
  26. package/src/shared/jsonApiTransports.js +79 -0
  27. package/src/shared/resources/workspaceInvitesResource.js +158 -0
  28. package/src/shared/resources/workspaceMembersResource.js +176 -311
  29. package/src/shared/resources/workspaceMembershipsResource.js +96 -0
  30. package/src/shared/resources/workspacePendingInvitationsResource.js +25 -72
  31. package/src/shared/resources/workspaceResource.js +113 -144
  32. package/src/shared/resources/workspaceRoleCatalogSchema.js +31 -0
  33. package/src/shared/resources/workspaceSettingsResource.js +276 -148
  34. package/test/repositoryContracts.test.js +16 -4
  35. package/test/resourcesCanonical.test.js +39 -16
  36. package/test/routeParamsValidator.test.js +37 -19
  37. package/test/usersRouteResources.test.js +27 -17
  38. package/test/workspaceActionContextContributor.test.js +1 -1
  39. package/test/workspaceInternalCrudResources.test.js +98 -0
  40. package/test/workspaceInvitesRepository.test.js +196 -148
  41. package/test/workspaceMembersResource.test.js +35 -0
  42. package/test/workspaceMembershipsRepository.test.js +155 -115
  43. package/test/workspacePendingInvitationsResource.test.js +18 -23
  44. package/test/workspacePendingInvitationsService.test.js +2 -1
  45. package/test/workspaceServerScopeSupport.test.js +77 -3
  46. package/test/workspaceService.test.js +26 -5
  47. package/test/workspaceSettingsActions.test.js +5 -7
  48. package/test/workspaceSettingsInternalResource.test.js +8 -0
  49. package/test/workspaceSettingsRepository.test.js +158 -123
  50. package/test/workspaceSettingsResource.test.js +51 -62
  51. package/test/workspaceSettingsService.test.js +0 -1
  52. package/test/workspacesRepository.test.js +318 -174
  53. package/test/workspacesRouteRequestInputValidator.test.js +25 -11
  54. package/src/server/common/resources/workspaceInvitesResource.js +0 -207
  55. package/src/server/common/resources/workspaceMembershipsResource.js +0 -154
  56. package/src/server/common/resources/workspacesResource.js +0 -170
  57. package/src/server/common/validators/authenticatedUserValidator.js +0 -43
  58. package/src/shared/resources/resolveGlobalArrayRegistry.js +0 -6
  59. package/src/shared/resources/workspaceSettingsFields.js +0 -65
  60. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +0 -197
  61. package/test/settingsFieldRegistriesSingleton.test.js +0 -14
  62. package/test-support/registerDefaultSettingsFields.js +0 -1
@@ -1,119 +1,154 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import "../test-support/registerDefaultSettingsFields.js";
4
3
  import { toIsoString } from "@jskit-ai/database-runtime/shared";
5
4
  import { resolveWorkspaceThemePalettes } from "@jskit-ai/workspaces-core/shared/settings";
6
5
  import { createRepository } from "../src/server/workspaceSettings/workspaceSettingsRepository.js";
7
6
 
8
- function createDefaultWorkspaceSettings() {
9
- return true;
7
+ function createKnexStub() {
8
+ const knex = Object.assign(() => {
9
+ throw new Error("query execution not expected");
10
+ }, {
11
+ async transaction(work) {
12
+ return work({ trxId: "trx-1" });
13
+ }
14
+ });
15
+
16
+ return knex;
17
+ }
18
+
19
+ function normalizeWorkspaceColor(value) {
20
+ return typeof value === "string" ? value.toUpperCase() : value;
10
21
  }
11
22
 
12
- const DEFAULT_WORKSPACE_THEME = resolveWorkspaceThemePalettes({});
13
- const STUB_CREATED_AT = "2026-03-09 00:26:35.710";
23
+ function createWorkspaceSettingsApiStub(rowOverrides = {}) {
24
+ const DEFAULT_WORKSPACE_THEME = resolveWorkspaceThemePalettes({});
25
+ const STUB_CREATED_AT = "2026-03-09 00:26:35.710";
26
+ const STUB_CREATED_AT_ISO = toIsoString(STUB_CREATED_AT);
14
27
 
15
- function createKnexStub(rowOverrides = {}) {
16
28
  const state = {
17
- insertedRow: null,
18
- updatePayload: null,
29
+ postPayload: null,
30
+ patchPayload: null,
19
31
  row: {
20
- workspace_id: 1,
21
- light_primary_color: DEFAULT_WORKSPACE_THEME.light.color,
22
- light_secondary_color: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
23
- light_surface_color: DEFAULT_WORKSPACE_THEME.light.surfaceColor,
24
- light_surface_variant_color: DEFAULT_WORKSPACE_THEME.light.surfaceVariantColor,
25
- dark_primary_color: DEFAULT_WORKSPACE_THEME.dark.color,
26
- dark_secondary_color: DEFAULT_WORKSPACE_THEME.dark.secondaryColor,
27
- dark_surface_color: DEFAULT_WORKSPACE_THEME.dark.surfaceColor,
28
- dark_surface_variant_color: DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor,
29
- invites_enabled: 1,
30
- created_at: STUB_CREATED_AT,
31
- updated_at: STUB_CREATED_AT,
32
+ id: "1",
33
+ lightPrimaryColor: DEFAULT_WORKSPACE_THEME.light.color,
34
+ lightSecondaryColor: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
35
+ lightSurfaceColor: DEFAULT_WORKSPACE_THEME.light.surfaceColor,
36
+ lightSurfaceVariantColor: DEFAULT_WORKSPACE_THEME.light.surfaceVariantColor,
37
+ darkPrimaryColor: DEFAULT_WORKSPACE_THEME.dark.color,
38
+ darkSecondaryColor: DEFAULT_WORKSPACE_THEME.dark.secondaryColor,
39
+ darkSurfaceColor: DEFAULT_WORKSPACE_THEME.dark.surfaceColor,
40
+ darkSurfaceVariantColor: DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor,
41
+ invitesEnabled: true,
42
+ createdAt: STUB_CREATED_AT_ISO,
43
+ updatedAt: STUB_CREATED_AT_ISO,
32
44
  ...rowOverrides
33
45
  }
34
46
  };
35
47
 
36
- function tableBuilder(tableName) {
37
- assert.equal(tableName, "workspace_settings");
38
-
39
- return {
40
- insert(payload) {
41
- state.insertedRow = { ...payload };
42
- state.row = {
43
- workspace_id: payload.workspace_id,
44
- light_primary_color: payload.light_primary_color,
45
- light_secondary_color: payload.light_secondary_color,
46
- light_surface_color: payload.light_surface_color,
47
- light_surface_variant_color: payload.light_surface_variant_color,
48
- dark_primary_color: payload.dark_primary_color,
49
- dark_secondary_color: payload.dark_secondary_color,
50
- dark_surface_color: payload.dark_surface_color,
51
- dark_surface_variant_color: payload.dark_surface_variant_color,
52
- invites_enabled: payload.invites_enabled,
53
- created_at: "2026-03-10 00:00:00.000",
54
- updated_at: "2026-03-10 00:00:00.000"
55
- };
56
- return Promise.resolve([1]);
57
- },
58
- where(criteria) {
59
- assert.equal(typeof criteria, "object");
60
-
61
- return {
62
- first() {
63
- return Promise.resolve(state.row ? { ...state.row } : null);
64
- },
65
- update(payload) {
66
- state.updatePayload = payload;
67
- if (Object.hasOwn(payload, "invites_enabled")) {
68
- state.row.invites_enabled = payload.invites_enabled;
69
- }
70
- if (Object.hasOwn(payload, "light_primary_color")) {
71
- state.row.light_primary_color = payload.light_primary_color;
72
- }
73
- if (Object.hasOwn(payload, "light_secondary_color")) {
74
- state.row.light_secondary_color = payload.light_secondary_color;
75
- }
76
- if (Object.hasOwn(payload, "light_surface_color")) {
77
- state.row.light_surface_color = payload.light_surface_color;
78
- }
79
- if (Object.hasOwn(payload, "light_surface_variant_color")) {
80
- state.row.light_surface_variant_color = payload.light_surface_variant_color;
81
- }
82
- if (Object.hasOwn(payload, "dark_primary_color")) {
83
- state.row.dark_primary_color = payload.dark_primary_color;
84
- }
85
- if (Object.hasOwn(payload, "dark_secondary_color")) {
86
- state.row.dark_secondary_color = payload.dark_secondary_color;
87
- }
88
- if (Object.hasOwn(payload, "dark_surface_color")) {
89
- state.row.dark_surface_color = payload.dark_surface_color;
90
- }
91
- if (Object.hasOwn(payload, "dark_surface_variant_color")) {
92
- state.row.dark_surface_variant_color = payload.dark_surface_variant_color;
48
+ const api = {
49
+ resources: {
50
+ workspaceSettings: {
51
+ async query({ queryParams }) {
52
+ const id = String(queryParams?.filters?.id || "");
53
+ if (!state.row || (id && String(state.row.id) !== id)) {
54
+ return { data: [] };
55
+ }
56
+
57
+ return {
58
+ data: [{
59
+ type: "workspaceSettings",
60
+ id: String(state.row.id),
61
+ attributes: {
62
+ lightPrimaryColor: state.row.lightPrimaryColor,
63
+ lightSecondaryColor: state.row.lightSecondaryColor,
64
+ lightSurfaceColor: state.row.lightSurfaceColor,
65
+ lightSurfaceVariantColor: state.row.lightSurfaceVariantColor,
66
+ darkPrimaryColor: state.row.darkPrimaryColor,
67
+ darkSecondaryColor: state.row.darkSecondaryColor,
68
+ darkSurfaceColor: state.row.darkSurfaceColor,
69
+ darkSurfaceVariantColor: state.row.darkSurfaceVariantColor,
70
+ invitesEnabled: state.row.invitesEnabled,
71
+ createdAt: state.row.createdAt,
72
+ updatedAt: state.row.updatedAt
73
+ }
74
+ }]
75
+ };
76
+ },
77
+ async post(payload) {
78
+ assert.equal(payload?.simplified, false);
79
+ const inputRecord = payload?.inputRecord?.data || {};
80
+ const attributes = inputRecord.attributes || {};
81
+ state.postPayload = inputRecord;
82
+ state.row = {
83
+ id: String(inputRecord.id),
84
+ lightPrimaryColor: normalizeWorkspaceColor(attributes.lightPrimaryColor ?? DEFAULT_WORKSPACE_THEME.light.color),
85
+ lightSecondaryColor: normalizeWorkspaceColor(attributes.lightSecondaryColor ?? DEFAULT_WORKSPACE_THEME.light.secondaryColor),
86
+ lightSurfaceColor: normalizeWorkspaceColor(attributes.lightSurfaceColor ?? DEFAULT_WORKSPACE_THEME.light.surfaceColor),
87
+ lightSurfaceVariantColor: normalizeWorkspaceColor(attributes.lightSurfaceVariantColor ?? DEFAULT_WORKSPACE_THEME.light.surfaceVariantColor),
88
+ darkPrimaryColor: normalizeWorkspaceColor(attributes.darkPrimaryColor ?? DEFAULT_WORKSPACE_THEME.dark.color),
89
+ darkSecondaryColor: normalizeWorkspaceColor(attributes.darkSecondaryColor ?? DEFAULT_WORKSPACE_THEME.dark.secondaryColor),
90
+ darkSurfaceColor: normalizeWorkspaceColor(attributes.darkSurfaceColor ?? DEFAULT_WORKSPACE_THEME.dark.surfaceColor),
91
+ darkSurfaceVariantColor: normalizeWorkspaceColor(attributes.darkSurfaceVariantColor ?? DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor),
92
+ invitesEnabled: attributes.invitesEnabled ?? true,
93
+ createdAt: toIsoString("2026-03-10 00:00:00.000"),
94
+ updatedAt: toIsoString("2026-03-10 00:00:00.000")
95
+ };
96
+ return {
97
+ data: {
98
+ type: "workspaceSettings",
99
+ id: String(state.row.id),
100
+ attributes: {
101
+ ...state.row
102
+ }
93
103
  }
94
- if (Object.hasOwn(payload, "updated_at")) {
95
- state.row.updated_at = payload.updated_at;
104
+ };
105
+ },
106
+ async patch(payload) {
107
+ assert.equal(payload?.simplified, false);
108
+ const inputRecord = payload?.inputRecord?.data || {};
109
+ const attributes = inputRecord.attributes || {};
110
+ state.patchPayload = inputRecord;
111
+ state.row = {
112
+ ...state.row,
113
+ ...attributes,
114
+ ...(Object.hasOwn(attributes, "lightPrimaryColor") ? { lightPrimaryColor: normalizeWorkspaceColor(attributes.lightPrimaryColor) } : {}),
115
+ ...(Object.hasOwn(attributes, "lightSecondaryColor") ? { lightSecondaryColor: normalizeWorkspaceColor(attributes.lightSecondaryColor) } : {}),
116
+ ...(Object.hasOwn(attributes, "lightSurfaceColor") ? { lightSurfaceColor: normalizeWorkspaceColor(attributes.lightSurfaceColor) } : {}),
117
+ ...(Object.hasOwn(attributes, "lightSurfaceVariantColor") ? { lightSurfaceVariantColor: normalizeWorkspaceColor(attributes.lightSurfaceVariantColor) } : {}),
118
+ ...(Object.hasOwn(attributes, "darkPrimaryColor") ? { darkPrimaryColor: normalizeWorkspaceColor(attributes.darkPrimaryColor) } : {}),
119
+ ...(Object.hasOwn(attributes, "darkSecondaryColor") ? { darkSecondaryColor: normalizeWorkspaceColor(attributes.darkSecondaryColor) } : {}),
120
+ ...(Object.hasOwn(attributes, "darkSurfaceColor") ? { darkSurfaceColor: normalizeWorkspaceColor(attributes.darkSurfaceColor) } : {}),
121
+ ...(Object.hasOwn(attributes, "darkSurfaceVariantColor") ? { darkSurfaceVariantColor: normalizeWorkspaceColor(attributes.darkSurfaceVariantColor) } : {}),
122
+ id: String(inputRecord.id || state.row?.id || "")
123
+ };
124
+ return {
125
+ data: {
126
+ type: "workspaceSettings",
127
+ id: String(state.row.id),
128
+ attributes: {
129
+ ...state.row
130
+ }
96
131
  }
97
- return Promise.resolve(1);
98
- }
99
- };
132
+ };
133
+ }
100
134
  }
101
- };
102
- }
135
+ }
136
+ };
103
137
 
104
- return { knexStub: tableBuilder, state };
138
+ return { api, state, DEFAULT_WORKSPACE_THEME, STUB_CREATED_AT };
105
139
  }
106
140
 
107
- test("workspaceSettingsRepository.findByWorkspaceId maps the stored row", async () => {
108
- const { knexStub } = createKnexStub();
109
- const repository = createRepository(knexStub, {
110
- defaultInvitesEnabled: createDefaultWorkspaceSettings()
141
+ test("workspaceSettingsRepository.findByWorkspaceId returns the canonical workspace-settings row", async () => {
142
+ const { api, DEFAULT_WORKSPACE_THEME, STUB_CREATED_AT } = createWorkspaceSettingsApiStub();
143
+ const repository = createRepository({
144
+ api,
145
+ knex: createKnexStub()
111
146
  });
112
147
 
113
148
  const record = await repository.findByWorkspaceId("1");
114
149
 
115
150
  assert.deepEqual(record, {
116
- workspaceId: "1",
151
+ id: "1",
117
152
  lightPrimaryColor: DEFAULT_WORKSPACE_THEME.light.color,
118
153
  lightSecondaryColor: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
119
154
  lightSurfaceColor: DEFAULT_WORKSPACE_THEME.light.surfaceColor,
@@ -129,44 +164,40 @@ test("workspaceSettingsRepository.findByWorkspaceId maps the stored row", async
129
164
  });
130
165
 
131
166
  test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates invitesEnabled only", async () => {
132
- const { knexStub, state } = createKnexStub();
133
- const repository = createRepository(knexStub, {
134
- defaultInvitesEnabled: createDefaultWorkspaceSettings()
167
+ const { api, state } = createWorkspaceSettingsApiStub();
168
+ const repository = createRepository({
169
+ api,
170
+ knex: createKnexStub()
135
171
  });
136
172
 
137
173
  const updated = await repository.updateSettingsByWorkspaceId("1", {
138
174
  invitesEnabled: false
139
175
  });
140
176
 
141
- assert.equal(state.updatePayload.invites_enabled, false);
177
+ assert.equal(state.patchPayload.attributes?.invitesEnabled, false);
142
178
  assert.equal(updated.invitesEnabled, false);
143
179
  });
144
180
 
145
- test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defaults exactly", async () => {
146
- const { knexStub, state } = createKnexStub();
181
+ test("workspaceSettingsRepository.ensureForWorkspaceId delegates defaults to the resource create path", async () => {
182
+ const { api, state, DEFAULT_WORKSPACE_THEME } = createWorkspaceSettingsApiStub();
147
183
  state.row = null;
148
- const repository = createRepository(knexStub, {
149
- defaultInvitesEnabled: false
184
+ const repository = createRepository({
185
+ api,
186
+ knex: createKnexStub()
150
187
  });
151
188
 
152
189
  const record = await repository.ensureForWorkspaceId("5");
153
190
 
154
- assert.equal(state.insertedRow.workspace_id, "5");
155
- assert.equal(state.insertedRow.light_primary_color, DEFAULT_WORKSPACE_THEME.light.color);
156
- assert.equal(state.insertedRow.light_secondary_color, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
157
- assert.equal(state.insertedRow.light_surface_color, DEFAULT_WORKSPACE_THEME.light.surfaceColor);
158
- assert.equal(
159
- state.insertedRow.light_surface_variant_color,
160
- DEFAULT_WORKSPACE_THEME.light.surfaceVariantColor
161
- );
162
- assert.equal(state.insertedRow.dark_primary_color, DEFAULT_WORKSPACE_THEME.dark.color);
163
- assert.equal(state.insertedRow.dark_secondary_color, DEFAULT_WORKSPACE_THEME.dark.secondaryColor);
164
- assert.equal(state.insertedRow.dark_surface_color, DEFAULT_WORKSPACE_THEME.dark.surfaceColor);
165
- assert.equal(
166
- state.insertedRow.dark_surface_variant_color,
167
- DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor
168
- );
169
- assert.equal(state.insertedRow.invites_enabled, false);
191
+ assert.equal(state.postPayload.id, "5");
192
+ assert.equal(state.postPayload.attributes?.lightPrimaryColor, DEFAULT_WORKSPACE_THEME.light.color);
193
+ assert.equal(state.postPayload.attributes?.lightSecondaryColor, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
194
+ assert.equal(state.postPayload.attributes?.lightSurfaceColor, DEFAULT_WORKSPACE_THEME.light.surfaceColor);
195
+ assert.equal(state.postPayload.attributes?.lightSurfaceVariantColor, DEFAULT_WORKSPACE_THEME.light.surfaceVariantColor);
196
+ assert.equal(state.postPayload.attributes?.darkPrimaryColor, DEFAULT_WORKSPACE_THEME.dark.color);
197
+ assert.equal(state.postPayload.attributes?.darkSecondaryColor, DEFAULT_WORKSPACE_THEME.dark.secondaryColor);
198
+ assert.equal(state.postPayload.attributes?.darkSurfaceColor, DEFAULT_WORKSPACE_THEME.dark.surfaceColor);
199
+ assert.equal(state.postPayload.attributes?.darkSurfaceVariantColor, DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor);
200
+ assert.equal(state.postPayload.attributes?.invitesEnabled, true);
170
201
  assert.equal(record.lightPrimaryColor, DEFAULT_WORKSPACE_THEME.light.color);
171
202
  assert.equal(record.lightSecondaryColor, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
172
203
  assert.equal(record.lightSurfaceColor, DEFAULT_WORKSPACE_THEME.light.surfaceColor);
@@ -175,28 +206,32 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
175
206
  assert.equal(record.darkSecondaryColor, DEFAULT_WORKSPACE_THEME.dark.secondaryColor);
176
207
  assert.equal(record.darkSurfaceColor, DEFAULT_WORKSPACE_THEME.dark.surfaceColor);
177
208
  assert.equal(record.darkSurfaceVariantColor, DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor);
178
- assert.equal(record.invitesEnabled, false);
179
- assert.equal(record.workspaceId, "5");
209
+ assert.equal(record.invitesEnabled, true);
210
+ assert.equal(record.id, "5");
180
211
  });
181
212
 
182
- test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates workspace settings columns", async () => {
183
- const { knexStub, state } = createKnexStub();
184
- const repository = createRepository(knexStub, {
185
- defaultInvitesEnabled: true
213
+ test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates workspace settings fields", async () => {
214
+ const { api, state } = createWorkspaceSettingsApiStub();
215
+ const repository = createRepository({
216
+ api,
217
+ knex: createKnexStub()
186
218
  });
187
219
 
188
220
  const updated = await repository.updateSettingsByWorkspaceId("1", {
189
221
  lightPrimaryColor: "#123abc"
190
222
  });
191
223
 
192
- assert.equal(state.updatePayload.light_primary_color, "#123ABC");
224
+ assert.equal(state.patchPayload.attributes?.lightPrimaryColor, "#123abc");
193
225
  assert.equal(updated.lightPrimaryColor, "#123ABC");
194
226
  });
195
227
 
196
228
  test("workspaceSettingsRepository can be constructed without validating app config shape", () => {
197
- const { knexStub } = createKnexStub();
229
+ const { api } = createWorkspaceSettingsApiStub();
198
230
 
199
- const repository = createRepository(knexStub);
231
+ const repository = createRepository({
232
+ api,
233
+ knex: createKnexStub()
234
+ });
200
235
 
201
236
  assert.ok(repository);
202
237
  });
@@ -1,9 +1,12 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
4
- import "../test-support/registerDefaultSettingsFields.js";
4
+ import { resolveStructuredSchemaTransportSchema } from "@jskit-ai/kernel/shared/validators";
5
5
  import { resolveWorkspaceThemePalettes } from "@jskit-ai/workspaces-core/shared/settings";
6
- import { workspaceSettingsResource } from "../src/shared/resources/workspaceSettingsResource.js";
6
+ import {
7
+ WORKSPACE_SETTINGS_FIELD_KEYS,
8
+ workspaceSettingsResource
9
+ } from "../src/shared/resources/workspaceSettingsResource.js";
7
10
  import { createWorkspaceRoleCatalog } from "../src/shared/roles.js";
8
11
 
9
12
  function createRoleCatalog() {
@@ -41,13 +44,13 @@ function createRoleCatalog() {
41
44
  function parseBody(operation, payload = {}) {
42
45
  return validateOperationSection({
43
46
  operation,
44
- section: "bodyValidator",
47
+ section: "body",
45
48
  value: payload
46
49
  });
47
50
  }
48
51
 
49
- test("workspace settings patch body normalizes valid payload before validation", () => {
50
- const parsed = parseBody(workspaceSettingsResource.operations.patch, {
52
+ test("workspace settings patch body validates valid payload without reshaping it", async () => {
53
+ const parsed = await parseBody(workspaceSettingsResource.operations.patch, {
51
54
  lightPrimaryColor: "#0f6b54",
52
55
  lightSecondaryColor: "#0b4d3c",
53
56
  lightSurfaceColor: "#eef5f3",
@@ -62,10 +65,10 @@ test("workspace settings patch body normalizes valid payload before validation",
62
65
  assert.equal(parsed.ok, true);
63
66
  assert.deepEqual(parsed.fieldErrors, {});
64
67
  assert.deepEqual(parsed.value, {
65
- lightPrimaryColor: "#0F6B54",
66
- lightSecondaryColor: "#0B4D3C",
67
- lightSurfaceColor: "#EEF5F3",
68
- lightSurfaceVariantColor: "#DDEAE7",
68
+ lightPrimaryColor: "#0f6b54",
69
+ lightSecondaryColor: "#0b4d3c",
70
+ lightSurfaceColor: "#eef5f3",
71
+ lightSurfaceVariantColor: "#ddeae7",
69
72
  darkPrimaryColor: "#123456",
70
73
  darkSecondaryColor: "#234567",
71
74
  darkSurfaceColor: "#345678",
@@ -74,18 +77,17 @@ test("workspace settings patch body normalizes valid payload before validation",
74
77
  });
75
78
  });
76
79
 
77
- test("workspace settings patch body ignores unknown fields after normalization", () => {
78
- const parsed = parseBody(workspaceSettingsResource.operations.patch, {
80
+ test("workspace settings patch body rejects unknown fields", async () => {
81
+ const parsed = await parseBody(workspaceSettingsResource.operations.patch, {
79
82
  avatarUrl: "https://example.com/avatar.png"
80
83
  });
81
84
 
82
- assert.equal(parsed.ok, true);
83
- assert.deepEqual(parsed.fieldErrors, {});
84
- assert.deepEqual(parsed.value, {});
85
+ assert.equal(parsed.ok, false);
86
+ assert.equal(typeof parsed.fieldErrors.avatarUrl, "string");
85
87
  });
86
88
 
87
- test("workspace settings create body requires full-write fields", () => {
88
- const parsed = parseBody(workspaceSettingsResource.operations.create, {});
89
+ test("workspace settings create body requires full-write fields", async () => {
90
+ const parsed = await parseBody(workspaceSettingsResource.operations.create, {});
89
91
 
90
92
  assert.equal(parsed.ok, false);
91
93
  assert.equal(parsed.fieldErrors.lightPrimaryColor, "Light primary color is required.");
@@ -99,24 +101,17 @@ test("workspace settings create body requires full-write fields", () => {
99
101
  assert.equal(parsed.fieldErrors.invitesEnabled, "invitesEnabled is required.");
100
102
  });
101
103
 
102
- test("workspace settings output normalizes raw service payloads", () => {
104
+ test("workspace settings output schema accepts already-shaped service payloads", () => {
105
+ const outputSchema = resolveStructuredSchemaTransportSchema(workspaceSettingsResource.operations.view.output, {
106
+ context: "workspaceSettings.view.output",
107
+ defaultMode: "replace"
108
+ });
109
+ const settingsRef = outputSchema.properties.settings.allOf[0].$ref.replace(/^#\/definitions\//, "");
110
+ const settingsSchema = outputSchema.definitions[settingsRef];
103
111
  const expectedTheme = resolveWorkspaceThemePalettes({
104
112
  lightPrimaryColor: "#0F6B54"
105
113
  });
106
- const normalized = workspaceSettingsResource.operations.view.outputValidator.normalize({
107
- workspace: {
108
- id: "7",
109
- slug: " mercury ",
110
- ownerUserId: "9"
111
- },
112
- settings: {
113
- lightPrimaryColor: "#0f6b54",
114
- invitesEnabled: false
115
- },
116
- roleCatalog: createRoleCatalog()
117
- });
118
-
119
- assert.deepEqual(normalized, {
114
+ const payload = {
120
115
  workspace: {
121
116
  id: "7",
122
117
  slug: "mercury",
@@ -135,35 +130,29 @@ test("workspace settings output normalizes raw service payloads", () => {
135
130
  invitesAvailable: true,
136
131
  invitesEffective: false
137
132
  },
138
- roleCatalog: {
139
- collaborationEnabled: true,
140
- defaultInviteRole: "member",
141
- roles: [
142
- {
143
- id: "owner",
144
- assignable: false,
145
- permissions: ["*"]
146
- },
147
- {
148
- id: "admin",
149
- assignable: true,
150
- permissions: [
151
- "workspace.roles.view",
152
- "workspace.settings.view",
153
- "workspace.settings.update",
154
- "workspace.members.view",
155
- "workspace.members.invite",
156
- "workspace.members.manage",
157
- "workspace.invites.revoke"
158
- ]
159
- },
160
- {
161
- id: "member",
162
- assignable: true,
163
- permissions: ["workspace.settings.view"]
164
- }
165
- ],
166
- assignableRoleIds: ["admin", "member"]
167
- }
168
- });
133
+ roleCatalog: createRoleCatalog()
134
+ };
135
+
136
+ assert.equal(outputSchema.type, "object");
137
+ assert.equal(outputSchema.additionalProperties, false);
138
+ assert.equal(outputSchema.properties.workspace["x-json-rest-schema"]?.castType, "object");
139
+ assert.equal(outputSchema.properties.settings["x-json-rest-schema"]?.castType, "object");
140
+ assert.equal(outputSchema.properties.roleCatalog["x-json-rest-schema"]?.castType, "object");
141
+ assert.equal(settingsSchema.properties.lightPrimaryColor.type, "string");
142
+ assert.equal(payload.settings.lightPrimaryColor, "#0F6B54");
143
+ });
144
+
145
+ async function importWithIdentity(url, identity) {
146
+ return import(`${url.href}?identity=${identity}`);
147
+ }
148
+
149
+ test("workspace settings key exports stay stable across module identities", async () => {
150
+ const workspaceModuleUrl = new URL("../src/shared/resources/workspaceSettingsResource.js", import.meta.url);
151
+
152
+ const workspaceA = await importWithIdentity(workspaceModuleUrl, "workspace-a");
153
+ const workspaceB = await importWithIdentity(workspaceModuleUrl, "workspace-b");
154
+
155
+ assert.deepEqual(workspaceA.WORKSPACE_SETTINGS_FIELD_KEYS, workspaceB.WORKSPACE_SETTINGS_FIELD_KEYS);
156
+ assert.deepEqual(workspaceA.WORKSPACE_SETTINGS_FIELD_KEYS, WORKSPACE_SETTINGS_FIELD_KEYS);
157
+ assert.ok(Object.isFrozen(workspaceA.WORKSPACE_SETTINGS_FIELD_KEYS));
169
158
  });
@@ -1,6 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import "../test-support/registerDefaultSettingsFields.js";
4
3
  import { resolveWorkspaceThemePalettes } from "@jskit-ai/workspaces-core/shared/settings";
5
4
  import { createService } from "../src/server/workspaceSettings/workspaceSettingsService.js";
6
5