@jskit-ai/workspaces-core 0.1.30 → 0.1.32

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 (61) 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 +26 -24
  8. package/src/server/common/validators/routeParamsValidator.js +36 -53
  9. package/src/server/registerWorkspaceCore.js +6 -7
  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 +21 -3
  46. package/test/workspaceSettingsActions.test.js +5 -7
  47. package/test/workspaceSettingsInternalResource.test.js +8 -0
  48. package/test/workspaceSettingsRepository.test.js +158 -123
  49. package/test/workspaceSettingsResource.test.js +51 -62
  50. package/test/workspaceSettingsService.test.js +0 -1
  51. package/test/workspacesRepository.test.js +318 -174
  52. package/test/workspacesRouteRequestInputValidator.test.js +25 -11
  53. package/src/server/common/resources/workspaceInvitesResource.js +0 -207
  54. package/src/server/common/resources/workspaceMembershipsResource.js +0 -154
  55. package/src/server/common/resources/workspacesResource.js +0 -170
  56. package/src/server/common/validators/authenticatedUserValidator.js +0 -43
  57. package/src/shared/resources/resolveGlobalArrayRegistry.js +0 -6
  58. package/src/shared/resources/workspaceSettingsFields.js +0 -65
  59. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +0 -197
  60. package/test/settingsFieldRegistriesSingleton.test.js +0 -14
  61. package/test-support/registerDefaultSettingsFields.js +0 -1
@@ -1,169 +1,297 @@
1
- import { Type } from "typebox";
2
- import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
3
- import {
4
- normalizeObjectInput,
5
- createCursorListValidator,
6
- normalizeSettingsFieldInput,
7
- recordIdSchema
8
- } from "@jskit-ai/kernel/shared/validators";
9
- import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
10
- import { workspaceSettingsFields } from "./workspaceSettingsFields.js";
11
- import { createWorkspaceRoleCatalog } from "../roles.js";
1
+ import { createSchema } from "json-rest-schema";
2
+ import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
3
+ import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
4
+ import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
5
+ import { workspaceRoleCatalogSchema } from "./workspaceRoleCatalogSchema.js";
6
+ import { normalizeWorkspaceHexColor, DEFAULT_WORKSPACE_DARK_PALETTE, DEFAULT_WORKSPACE_LIGHT_PALETTE } from "../settings.js";
12
7
 
13
- function buildCreateBodySchema() {
14
- const properties = {};
15
- for (const field of workspaceSettingsFields) {
16
- properties[field.key] = field.required === false ? Type.Optional(field.inputSchema) : field.inputSchema;
17
- }
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
+ ]);
18
19
 
19
- return Type.Object(properties, {
20
- additionalProperties: false,
20
+ const workspaceSettingsBodySchema = createSchema({
21
+ lightPrimaryColor: {
22
+ type: "string",
23
+ required: true,
24
+ minLength: 7,
25
+ maxLength: 7,
26
+ pattern: "^#[0-9A-Fa-f]{6}$",
21
27
  messages: {
22
- additionalProperties: "Unexpected field.",
23
- default: "Invalid value."
28
+ required: "Light primary color is required.",
29
+ pattern: "Light primary color must be a hex color like #1867C0.",
30
+ default: "Light primary color must be a hex color like #1867C0."
31
+ }
32
+ },
33
+ lightSecondaryColor: {
34
+ type: "string",
35
+ required: true,
36
+ minLength: 7,
37
+ maxLength: 7,
38
+ pattern: "^#[0-9A-Fa-f]{6}$",
39
+ messages: {
40
+ required: "Light secondary color is required.",
41
+ pattern: "Light secondary color must be a hex color like #48A9A6.",
42
+ default: "Light secondary color must be a hex color like #48A9A6."
43
+ }
44
+ },
45
+ lightSurfaceColor: {
46
+ type: "string",
47
+ required: true,
48
+ minLength: 7,
49
+ maxLength: 7,
50
+ pattern: "^#[0-9A-Fa-f]{6}$",
51
+ messages: {
52
+ required: "Light surface color is required.",
53
+ pattern: "Light surface color must be a hex color like #FFFFFF.",
54
+ default: "Light surface color must be a hex color like #FFFFFF."
55
+ }
56
+ },
57
+ lightSurfaceVariantColor: {
58
+ type: "string",
59
+ required: true,
60
+ minLength: 7,
61
+ maxLength: 7,
62
+ pattern: "^#[0-9A-Fa-f]{6}$",
63
+ messages: {
64
+ required: "Light surface variant color is required.",
65
+ pattern: "Light surface variant color must be a hex color like #424242.",
66
+ default: "Light surface variant color must be a hex color like #424242."
67
+ }
68
+ },
69
+ darkPrimaryColor: {
70
+ type: "string",
71
+ required: true,
72
+ minLength: 7,
73
+ maxLength: 7,
74
+ pattern: "^#[0-9A-Fa-f]{6}$",
75
+ messages: {
76
+ required: "Dark primary color is required.",
77
+ pattern: "Dark primary color must be a hex color like #2196F3.",
78
+ default: "Dark primary color must be a hex color like #2196F3."
79
+ }
80
+ },
81
+ darkSecondaryColor: {
82
+ type: "string",
83
+ required: true,
84
+ minLength: 7,
85
+ maxLength: 7,
86
+ pattern: "^#[0-9A-Fa-f]{6}$",
87
+ messages: {
88
+ required: "Dark secondary color is required.",
89
+ pattern: "Dark secondary color must be a hex color like #54B6B2.",
90
+ default: "Dark secondary color must be a hex color like #54B6B2."
91
+ }
92
+ },
93
+ darkSurfaceColor: {
94
+ type: "string",
95
+ required: true,
96
+ minLength: 7,
97
+ maxLength: 7,
98
+ pattern: "^#[0-9A-Fa-f]{6}$",
99
+ messages: {
100
+ required: "Dark surface color is required.",
101
+ pattern: "Dark surface color must be a hex color like #212121.",
102
+ default: "Dark surface color must be a hex color like #212121."
103
+ }
104
+ },
105
+ darkSurfaceVariantColor: {
106
+ type: "string",
107
+ required: true,
108
+ minLength: 7,
109
+ maxLength: 7,
110
+ pattern: "^#[0-9A-Fa-f]{6}$",
111
+ messages: {
112
+ required: "Dark surface variant color is required.",
113
+ pattern: "Dark surface variant color must be a hex color like #C8C8C8.",
114
+ default: "Dark surface variant color must be a hex color like #C8C8C8."
115
+ }
116
+ },
117
+ invitesEnabled: {
118
+ type: "boolean",
119
+ required: true,
120
+ strictBoolean: true,
121
+ messages: {
122
+ required: "invitesEnabled is required.",
123
+ default: "invitesEnabled must be a boolean."
24
124
  }
25
- });
26
- }
27
-
28
- function buildSettingsOutputSchema() {
29
- const properties = {};
30
- for (const field of workspaceSettingsFields) {
31
- properties[field.key] = field.outputSchema;
32
125
  }
33
- properties.invitesAvailable = Type.Boolean();
34
- properties.invitesEffective = Type.Boolean();
35
-
36
- return Type.Object(properties, { additionalProperties: false });
37
- }
38
-
39
- function buildResponseRecordSchema() {
40
- return Type.Object(
41
- {
42
- workspace: Type.Object(
43
- {
44
- id: recordIdSchema,
45
- slug: Type.String({ minLength: 1 }),
46
- ownerUserId: recordIdSchema
47
- },
48
- { additionalProperties: false }
49
- ),
50
- settings: buildSettingsOutputSchema(),
51
- roleCatalog: Type.Object(
52
- {
53
- collaborationEnabled: Type.Boolean(),
54
- defaultInviteRole: Type.String(),
55
- roles: Type.Array(Type.Object({}, { additionalProperties: true })),
56
- assignableRoleIds: Type.Array(Type.String({ minLength: 1 }))
57
- },
58
- { additionalProperties: true }
59
- )
60
- },
61
- { additionalProperties: false }
62
- );
63
- }
126
+ });
64
127
 
65
- function normalizeInput(payload = {}) {
66
- return normalizeSettingsFieldInput(payload, workspaceSettingsFields);
67
- }
128
+ const workspaceSettingsWorkspaceOutputSchema = createSchema({
129
+ id: { type: "string", required: true, minLength: 1, pattern: RECORD_ID_PATTERN },
130
+ slug: { type: "string", required: true, minLength: 1, maxLength: 120 },
131
+ ownerUserId: { type: "string", required: true, minLength: 1, pattern: RECORD_ID_PATTERN }
132
+ });
68
133
 
69
- function normalizeOutput(payload = {}) {
70
- const source = normalizeObjectInput(payload);
71
- const workspace = normalizeObjectInput(source.workspace);
72
- const settings = normalizeObjectInput(source.settings);
73
- const normalizedSettings = {};
134
+ const workspaceSettingsViewSchema = createSchema({
135
+ lightPrimaryColor: { type: "string", required: true, minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" },
136
+ lightSecondaryColor: { type: "string", required: true, minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" },
137
+ lightSurfaceColor: { type: "string", required: true, minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" },
138
+ lightSurfaceVariantColor: { type: "string", required: true, minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" },
139
+ darkPrimaryColor: { type: "string", required: true, minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" },
140
+ darkSecondaryColor: { type: "string", required: true, minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" },
141
+ darkSurfaceColor: { type: "string", required: true, minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" },
142
+ darkSurfaceVariantColor: { type: "string", required: true, minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" },
143
+ invitesEnabled: { type: "boolean", required: true },
144
+ invitesAvailable: { type: "boolean", required: true },
145
+ invitesEffective: { type: "boolean", required: true }
146
+ });
74
147
 
75
- for (const field of workspaceSettingsFields) {
76
- const rawValue = Object.hasOwn(settings, field.key)
77
- ? settings[field.key]
78
- : field.resolveDefault({
79
- workspace,
80
- settings
81
- });
82
- normalizedSettings[field.key] = field.normalizeOutput(rawValue, {
83
- workspace,
84
- settings
85
- });
148
+ const workspaceSettingsOutputSchema = createSchema({
149
+ workspace: {
150
+ type: "object",
151
+ required: true,
152
+ schema: workspaceSettingsWorkspaceOutputSchema
153
+ },
154
+ settings: {
155
+ type: "object",
156
+ required: true,
157
+ schema: workspaceSettingsViewSchema
158
+ },
159
+ roleCatalog: {
160
+ type: "object",
161
+ required: true,
162
+ schema: workspaceRoleCatalogSchema
86
163
  }
164
+ });
87
165
 
88
- const invitesEnabled = normalizedSettings.invitesEnabled !== false;
89
- const invitesAvailable = settings.invitesAvailable !== false;
90
- const invitesEffective =
91
- typeof settings.invitesEffective === "boolean" ? settings.invitesEffective : invitesEnabled;
92
- normalizedSettings.invitesEnabled = invitesEnabled;
93
- normalizedSettings.invitesAvailable = invitesAvailable;
94
- normalizedSettings.invitesEffective = invitesEffective;
95
- const roleCatalog = normalizeObjectInput(source.roleCatalog);
96
- const hasRoleCatalog =
97
- Array.isArray(roleCatalog.roles) &&
98
- roleCatalog.roles.length > 0 &&
99
- Array.isArray(roleCatalog.assignableRoleIds);
100
-
101
- return {
102
- workspace: {
103
- id: normalizeRecordId(workspace.id, { fallback: "" }),
104
- slug: normalizeText(workspace.slug),
105
- ownerUserId: normalizeRecordId(workspace.ownerUserId, { fallback: "" })
106
- },
107
- settings: normalizedSettings,
108
- roleCatalog: hasRoleCatalog ? roleCatalog : createWorkspaceRoleCatalog()
109
- };
166
+ function normalizeWorkspaceColorInput(value) {
167
+ return normalizeWorkspaceHexColor(value);
110
168
  }
111
169
 
112
- const responseRecordValidator = Object.freeze({
113
- get schema() {
114
- return buildResponseRecordSchema();
115
- },
116
- normalize: normalizeOutput
117
- });
118
-
119
- const resource = {
170
+ const workspaceSettingsResource = defineCrudResource({
120
171
  namespace: "workspaceSettings",
121
- messages: {
122
- validation: "Fix invalid workspace settings values and try again.",
123
- saveSuccess: "Workspace settings updated.",
124
- saveError: "Unable to update workspace settings.",
125
- apiValidation: "Validation failed."
172
+ tableName: "workspace_settings",
173
+ idProperty: "workspace_id",
174
+ searchSchema: {
175
+ id: { type: "id", actualField: "id" }
126
176
  },
127
- operations: {
128
- view: {
129
- method: "GET",
130
- outputValidator: responseRecordValidator
177
+ schema: {
178
+ id: {
179
+ type: "id",
180
+ primary: true,
181
+ required: true,
182
+ search: true,
183
+ storage: { column: "workspace_id" }
184
+ },
185
+ lightPrimaryColor: {
186
+ type: "string",
187
+ required: true,
188
+ minLength: 7,
189
+ maxLength: 7,
190
+ defaultTo: DEFAULT_WORKSPACE_LIGHT_PALETTE.color,
191
+ storage: { column: "light_primary_color" },
192
+ setter: (value) => normalizeWorkspaceColorInput(value)
131
193
  },
132
- list: {
133
- method: "GET",
134
- outputValidator: createCursorListValidator(responseRecordValidator)
194
+ lightSecondaryColor: {
195
+ type: "string",
196
+ required: true,
197
+ minLength: 7,
198
+ maxLength: 7,
199
+ defaultTo: DEFAULT_WORKSPACE_LIGHT_PALETTE.secondaryColor,
200
+ storage: { column: "light_secondary_color" },
201
+ setter: (value) => normalizeWorkspaceColorInput(value)
135
202
  },
136
- create: {
137
- method: "POST",
138
- bodyValidator: {
139
- get schema() {
140
- return buildCreateBodySchema();
141
- },
142
- normalize: normalizeInput
143
- },
144
- outputValidator: responseRecordValidator
203
+ lightSurfaceColor: {
204
+ type: "string",
205
+ required: true,
206
+ minLength: 7,
207
+ maxLength: 7,
208
+ defaultTo: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceColor,
209
+ storage: { column: "light_surface_color" },
210
+ setter: (value) => normalizeWorkspaceColorInput(value)
145
211
  },
146
- replace: {
147
- method: "PUT",
148
- bodyValidator: {
149
- get schema() {
150
- return buildCreateBodySchema();
151
- },
152
- normalize: normalizeInput
153
- },
154
- outputValidator: responseRecordValidator
212
+ lightSurfaceVariantColor: {
213
+ type: "string",
214
+ required: true,
215
+ minLength: 7,
216
+ maxLength: 7,
217
+ defaultTo: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceVariantColor,
218
+ storage: { column: "light_surface_variant_color" },
219
+ setter: (value) => normalizeWorkspaceColorInput(value)
155
220
  },
156
- patch: {
157
- method: "PATCH",
158
- bodyValidator: {
159
- get schema() {
160
- return Type.Partial(buildCreateBodySchema(), { additionalProperties: false });
161
- },
162
- normalize: normalizeInput
163
- },
164
- outputValidator: responseRecordValidator
221
+ darkPrimaryColor: {
222
+ type: "string",
223
+ required: true,
224
+ minLength: 7,
225
+ maxLength: 7,
226
+ defaultTo: DEFAULT_WORKSPACE_DARK_PALETTE.color,
227
+ storage: { column: "dark_primary_color" },
228
+ setter: (value) => normalizeWorkspaceColorInput(value)
229
+ },
230
+ darkSecondaryColor: {
231
+ type: "string",
232
+ required: true,
233
+ minLength: 7,
234
+ maxLength: 7,
235
+ defaultTo: DEFAULT_WORKSPACE_DARK_PALETTE.secondaryColor,
236
+ storage: { column: "dark_secondary_color" },
237
+ setter: (value) => normalizeWorkspaceColorInput(value)
238
+ },
239
+ darkSurfaceColor: {
240
+ type: "string",
241
+ required: true,
242
+ minLength: 7,
243
+ maxLength: 7,
244
+ defaultTo: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceColor,
245
+ storage: { column: "dark_surface_color" },
246
+ setter: (value) => normalizeWorkspaceColorInput(value)
247
+ },
248
+ darkSurfaceVariantColor: {
249
+ type: "string",
250
+ required: true,
251
+ minLength: 7,
252
+ maxLength: 7,
253
+ defaultTo: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceVariantColor,
254
+ storage: { column: "dark_surface_variant_color" },
255
+ setter: (value) => normalizeWorkspaceColorInput(value)
256
+ },
257
+ invitesEnabled: {
258
+ type: "boolean",
259
+ required: true,
260
+ defaultTo: true,
261
+ storage: { column: "invites_enabled" }
262
+ },
263
+ createdAt: {
264
+ type: "dateTime",
265
+ default: "now()",
266
+ storage: {
267
+ column: "created_at",
268
+ writeSerializer: "datetime-utc"
269
+ }
270
+ },
271
+ updatedAt: {
272
+ type: "dateTime",
273
+ default: "now()",
274
+ storage: {
275
+ column: "updated_at",
276
+ writeSerializer: "datetime-utc"
277
+ }
165
278
  }
279
+ },
280
+ messages: {
281
+ validation: "Fix invalid workspace settings values and try again.",
282
+ saveSuccess: "Workspace settings updated.",
283
+ saveError: "Unable to update workspace settings.",
284
+ apiValidation: "Validation failed."
285
+ },
286
+ crudOperations: ["view", "list", "create", "replace", "patch"],
287
+ crud: {
288
+ output: workspaceSettingsOutputSchema,
289
+ body: workspaceSettingsBodySchema
166
290
  }
167
- };
291
+ });
168
292
 
169
- export { resource as workspaceSettingsResource };
293
+ export {
294
+ WORKSPACE_SETTINGS_FIELD_KEYS,
295
+ workspaceSettingsOutputSchema,
296
+ workspaceSettingsResource
297
+ };
@@ -17,13 +17,25 @@ function createKnexStub() {
17
17
  return knex;
18
18
  }
19
19
 
20
+ function createApiStub() {
21
+ return {
22
+ resources: {
23
+ workspaceInvites: {},
24
+ workspaceMemberships: {},
25
+ workspaces: {},
26
+ workspaceSettings: {}
27
+ }
28
+ };
29
+ }
30
+
20
31
  test("workspaces-core repositories expose withTransaction", async () => {
21
32
  const knex = createKnexStub();
33
+ const api = createApiStub();
22
34
  const repositories = [
23
- createWorkspaceInvitesRepository(knex),
24
- createWorkspaceMembershipsRepository(knex),
25
- createWorkspacesRepository(knex),
26
- createWorkspaceSettingsRepository(knex)
35
+ createWorkspaceInvitesRepository({ api, knex }),
36
+ createWorkspaceMembershipsRepository({ api, knex }),
37
+ createWorkspacesRepository({ api, knex }),
38
+ createWorkspaceSettingsRepository({ api, knex })
27
39
  ];
28
40
 
29
41
  for (const repository of repositories) {
@@ -3,8 +3,10 @@ import assert from "node:assert/strict";
3
3
  import path from "node:path";
4
4
  import { existsSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
6
- import "../test-support/registerDefaultSettingsFields.js";
6
+ import { resolveStructuredSchemaTransportSchema } from "@jskit-ai/kernel/shared/validators";
7
7
  import { workspaceMembersResource } from "../src/shared/resources/workspaceMembersResource.js";
8
+ import { workspaceInvitesResource } from "../src/shared/resources/workspaceInvitesResource.js";
9
+ import { workspaceMembershipsResource } from "../src/shared/resources/workspaceMembershipsResource.js";
8
10
  import { workspaceResource } from "../src/shared/resources/workspaceResource.js";
9
11
  import { workspaceSettingsResource } from "../src/shared/resources/workspaceSettingsResource.js";
10
12
 
@@ -28,13 +30,27 @@ function assertResourceOperationMessages(resource, operationName, label) {
28
30
 
29
31
  test("workspaces-core resources expose messages for all operations", () => {
30
32
  const resources = {
31
- workspace: workspaceResource,
32
- workspaceSettings: workspaceSettingsResource
33
+ workspace: {
34
+ resource: workspaceResource,
35
+ operations: ["view", "list", "create", "replace", "patch"]
36
+ },
37
+ workspaceSettings: {
38
+ resource: workspaceSettingsResource,
39
+ operations: ["view", "list", "create", "replace", "patch"]
40
+ },
41
+ workspaceMemberships: {
42
+ resource: workspaceMembershipsResource,
43
+ operations: ["view", "list", "create", "patch"]
44
+ },
45
+ workspaceInvites: {
46
+ resource: workspaceInvitesResource,
47
+ operations: ["view", "list", "create", "patch"]
48
+ }
33
49
  };
34
50
 
35
- for (const [label, resource] of Object.entries(resources)) {
36
- for (const operationName of ["view", "list", "create", "replace", "patch"]) {
37
- assertResourceOperationMessages(resource, operationName, label);
51
+ for (const [label, spec] of Object.entries(resources)) {
52
+ for (const operationName of spec.operations) {
53
+ assertResourceOperationMessages(spec.resource, operationName, label);
38
54
  }
39
55
  }
40
56
  });
@@ -53,22 +69,29 @@ test("workspaces-core specialized resource operations expose messages and valida
53
69
 
54
70
  for (const { label, operation } of operationSpecs) {
55
71
  assert.equal(typeof operation?.messages, "object", `${label}.messages must be an object.`);
56
- assert.equal(typeof operation?.outputValidator?.schema, "object", `${label}.outputValidator.schema must exist.`);
57
- if (operation?.bodyValidator) {
58
- assert.equal(typeof operation.bodyValidator.schema, "object", `${label}.bodyValidator.schema must exist.`);
72
+ assert.equal(
73
+ typeof resolveStructuredSchemaTransportSchema(operation?.output, {
74
+ context: `${label}.output`,
75
+ defaultMode: "replace"
76
+ }),
77
+ "object",
78
+ `${label}.output transport schema must exist.`
79
+ );
80
+ if (operation?.body) {
81
+ assert.equal(typeof operation.body.schema, "object", `${label}.body.schema must exist.`);
59
82
  }
60
- if (operation?.paramsValidator) {
61
- assert.equal(typeof operation.paramsValidator.schema, "object", `${label}.paramsValidator.schema must exist.`);
83
+ if (operation?.params) {
84
+ assert.equal(typeof operation.params.schema, "object", `${label}.params.schema must exist.`);
62
85
  }
63
- if (operation?.queryValidator) {
64
- assert.equal(typeof operation.queryValidator.schema, "object", `${label}.queryValidator.schema must exist.`);
86
+ if (operation?.query) {
87
+ assert.equal(typeof operation.query.schema, "object", `${label}.query.schema must exist.`);
65
88
  }
66
89
  }
67
90
  });
68
91
 
69
- test("workspaces-core no longer contains legacy shared/schema directory", () => {
92
+ test("workspaces-core does not contain src/shared/schema", () => {
70
93
  const testFilePath = fileURLToPath(import.meta.url);
71
94
  const packageRoot = path.resolve(path.dirname(testFilePath), "..");
72
- const legacySchemaDir = path.join(packageRoot, "src", "shared", "schema");
73
- assert.equal(existsSync(legacySchemaDir), false, "src/shared/schema must not exist.");
95
+ const sharedSchemaDirPath = path.join(packageRoot, "src", "shared", "schema");
96
+ assert.equal(existsSync(sharedSchemaDirPath), false, "src/shared/schema must not exist.");
74
97
  });
@@ -1,36 +1,53 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { Type } from "@fastify/type-provider-typebox";
3
+ import { createSchema } from "json-rest-schema";
4
4
  import { compileRouteValidator } from "@jskit-ai/kernel/_testable";
5
5
  import { routeParamsValidator } from "../src/server/common/validators/routeParamsValidator.js";
6
6
 
7
- test("routeParamsValidator exposes a shared workspace route params validator", () => {
7
+ function composeSchemaDefinition(...definitions) {
8
+ return Object.freeze({
9
+ schema: createSchema(
10
+ Object.assign({}, ...definitions.map((definition) => definition.schema.getFieldDefinitions()))
11
+ ),
12
+ mode: "patch"
13
+ });
14
+ }
15
+
16
+ test("routeParamsValidator exposes a shared workspace route params schema definition", () => {
8
17
  assert.equal(typeof routeParamsValidator.schema, "object");
9
- assert.equal(typeof routeParamsValidator.normalize, "function");
18
+ assert.equal(routeParamsValidator.mode, "patch");
10
19
  });
11
20
 
12
- test("workspace route validator pipeline uses the shared params validator and merges query arrays automatically", () => {
21
+ test("workspace route validator pipeline uses the shared params validator with a composed query schema", () => {
13
22
  const paginationQueryValidator = Object.freeze({
14
- schema: Type.Object(
15
- {
16
- cursor: Type.Optional(Type.String({ minLength: 1 })),
17
- limit: Type.Optional(Type.String({ pattern: "^[0-9]+$" }))
23
+ schema: createSchema({
24
+ cursor: {
25
+ type: "string",
26
+ required: false,
27
+ minLength: 1
18
28
  },
19
- { additionalProperties: false }
20
- )
29
+ limit: {
30
+ type: "string",
31
+ required: false,
32
+ pattern: "^[0-9]+$"
33
+ }
34
+ }),
35
+ mode: "patch"
21
36
  });
22
37
  const searchQueryValidator = Object.freeze({
23
- schema: Type.Object(
24
- {
25
- search: Type.Optional(Type.String({ minLength: 1 }))
26
- },
27
- { additionalProperties: false }
28
- )
38
+ schema: createSchema({
39
+ search: {
40
+ type: "string",
41
+ required: false,
42
+ minLength: 1
43
+ }
44
+ }),
45
+ mode: "patch"
29
46
  });
30
47
 
31
48
  const compiled = compileRouteValidator({
32
- paramsValidator: routeParamsValidator,
33
- queryValidator: [paginationQueryValidator, searchQueryValidator]
49
+ params: routeParamsValidator,
50
+ query: composeSchemaDefinition(paginationQueryValidator, searchQueryValidator)
34
51
  });
35
52
 
36
53
  assert.equal(compiled.schema.params.type, "object");
@@ -39,7 +56,8 @@ test("workspace route validator pipeline uses the shared params validator and me
39
56
  assert.equal(typeof compiled.schema.params.properties.memberUserId, "object");
40
57
  assert.equal(typeof compiled.schema.params.properties.inviteId, "object");
41
58
  assert.equal(typeof compiled.schema.params.properties.provider, "object");
42
- assert.equal(compiled.input.params({ workspaceSlug: " ACME " }).workspaceSlug, "acme");
59
+ const normalizedParams = compiled.input.params({ workspaceSlug: "ACME" });
60
+ assert.equal(normalizedParams.workspaceSlug, "acme");
43
61
 
44
62
  assert.equal(compiled.schema.querystring.type, "object");
45
63
  assert.equal(compiled.schema.querystring.additionalProperties, false);