@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,82 +1,35 @@
1
- import { Type } from "@fastify/type-provider-typebox";
2
- import { encodeInviteTokenHash } from "@jskit-ai/auth-core/shared/inviteTokens";
3
- import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
1
+ import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
2
+ import { createSchema } from "json-rest-schema";
3
+ import { defineResource } from "@jskit-ai/resource-core/shared/resource";
4
4
  import { createOperationMessages } from "../operationMessages.js";
5
- import { normalizeObjectInput, recordIdSchema } from "@jskit-ai/kernel/shared/validators";
6
- import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
7
5
 
8
- function normalizePendingInvite(invite) {
9
- const id = normalizeRecordId(invite?.id, { fallback: null });
10
- const workspaceId = normalizeRecordId(invite?.workspaceId, { fallback: null });
11
- const tokenHash = normalizeText(invite?.tokenHash);
12
-
13
- if (!id || !workspaceId || !tokenHash) {
14
- return null;
15
- }
16
-
17
- return {
18
- id,
19
- workspaceId,
20
- workspaceSlug: normalizeText(invite?.workspaceSlug),
21
- workspaceName: normalizeText(invite?.workspaceName || invite?.workspaceSlug),
22
- workspaceAvatarUrl: normalizeText(invite?.workspaceAvatarUrl),
23
- roleSid: normalizeLowerText(invite?.roleSid || "member") || "member",
24
- status: normalizeLowerText(invite?.status || "pending") || "pending",
25
- expiresAt: invite?.expiresAt || null,
26
- token: encodeInviteTokenHash(tokenHash)
27
- };
28
- }
29
-
30
- function normalizePendingInviteList(invites) {
31
- return (Array.isArray(invites) ? invites : []).map((invite) => normalizePendingInvite(invite)).filter(Boolean);
32
- }
33
-
34
- const pendingInviteRecordValidator = Object.freeze({
35
- schema: Type.Object(
36
- {
37
- id: recordIdSchema,
38
- workspaceId: recordIdSchema,
39
- workspaceSlug: Type.String({ minLength: 1 }),
40
- workspaceName: Type.String({ minLength: 1 }),
41
- workspaceAvatarUrl: Type.String(),
42
- roleSid: Type.String({ minLength: 1 }),
43
- status: Type.String({ minLength: 1 }),
44
- expiresAt: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
45
- token: Type.String({ minLength: 1 })
46
- },
47
- { additionalProperties: false }
48
- ),
49
- normalize: normalizePendingInvite
50
- });
51
-
52
- const pendingInvitationsListOutputValidator = Object.freeze({
53
- schema: Type.Object(
54
- {
55
- pendingInvites: Type.Array(pendingInviteRecordValidator.schema)
56
- },
57
- { additionalProperties: false }
58
- ),
59
- normalize(payload = {}) {
60
- const source = normalizeObjectInput(payload);
61
-
62
- return {
63
- pendingInvites: normalizePendingInviteList(source.pendingInvites)
64
- };
6
+ const pendingInvitationsListOutputSchema = createSchema({
7
+ pendingInvites: {
8
+ type: "array",
9
+ required: true,
10
+ items: createSchema({
11
+ id: { type: "string", required: true, minLength: 1, pattern: RECORD_ID_PATTERN },
12
+ workspaceId: { type: "string", required: true, minLength: 1, pattern: RECORD_ID_PATTERN },
13
+ workspaceSlug: { type: "string", required: true, minLength: 1, maxLength: 120 },
14
+ workspaceName: { type: "string", required: true, minLength: 1, maxLength: 160 },
15
+ workspaceAvatarUrl: { type: "string", required: true },
16
+ roleSid: { type: "string", required: true, minLength: 1, maxLength: 64 },
17
+ status: { type: "string", required: true, minLength: 1, maxLength: 64 },
18
+ expiresAt: { type: "string", required: false, nullable: true, minLength: 1 },
19
+ token: { type: "string", required: true, minLength: 1 }
20
+ })
65
21
  }
66
22
  });
67
23
 
68
- const WORKSPACE_PENDING_INVITATIONS_MESSAGES = createOperationMessages();
69
-
70
- const workspacePendingInvitationsResource = Object.freeze({
24
+ const workspacePendingInvitationsResource = defineResource({
71
25
  namespace: "workspacePendingInvitations",
72
- messages: WORKSPACE_PENDING_INVITATIONS_MESSAGES,
73
- operations: Object.freeze({
74
- list: Object.freeze({
26
+ messages: createOperationMessages(),
27
+ operations: {
28
+ list: {
75
29
  method: "GET",
76
- messages: WORKSPACE_PENDING_INVITATIONS_MESSAGES,
77
- outputValidator: pendingInvitationsListOutputValidator
78
- })
79
- })
30
+ output: pendingInvitationsListOutputSchema
31
+ }
32
+ }
80
33
  });
81
34
 
82
35
  export { workspacePendingInvitationsResource };
@@ -1,12 +1,8 @@
1
- import { Type } from "typebox";
2
- import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
3
- import {
4
- normalizeObjectInput,
5
- createCursorListValidator,
6
- recordIdSchema,
7
- recordIdInputSchema
8
- } from "@jskit-ai/kernel/shared/validators";
9
- import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
1
+ import { createSchema } from "json-rest-schema";
2
+ import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
4
+ import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
5
+ import { createSchemaDefinition } from "@jskit-ai/resource-core/shared/resource";
10
6
 
11
7
  function normalizeWorkspaceAvatarUrl(value) {
12
8
  const avatarUrl = normalizeText(value);
@@ -23,154 +19,127 @@ function normalizeWorkspaceAvatarUrl(value) {
23
19
  }
24
20
  }
25
21
 
26
- function normalizeWorkspaceInput(payload = {}) {
27
- const source = normalizeObjectInput(payload);
28
- const normalized = {};
29
-
30
- if (Object.hasOwn(source, "slug")) {
31
- normalized.slug = normalizeLowerText(source.slug);
32
- }
33
- if (Object.hasOwn(source, "name")) {
34
- normalized.name = normalizeText(source.name);
35
- }
36
- if (Object.hasOwn(source, "ownerUserId")) {
37
- normalized.ownerUserId = normalizeRecordId(source.ownerUserId, { fallback: "" });
38
- }
39
- if (Object.hasOwn(source, "avatarUrl")) {
40
- normalized.avatarUrl = normalizeWorkspaceAvatarUrl(source.avatarUrl);
41
- }
42
- if (Object.hasOwn(source, "isPersonal")) {
43
- normalized.isPersonal = source.isPersonal === true;
44
- }
45
-
46
- return normalized;
47
- }
48
-
49
- function normalizeWorkspaceOutput(payload = {}) {
50
- const source = normalizeObjectInput(payload);
51
-
52
- return {
53
- id: normalizeRecordId(source.id, { fallback: "" }),
54
- slug: normalizeLowerText(source.slug),
55
- name: normalizeText(source.name),
56
- ownerUserId: normalizeRecordId(source.ownerUserId, { fallback: "" }),
57
- avatarUrl: normalizeText(source.avatarUrl)
58
- };
59
- }
60
-
61
- function normalizeWorkspaceListItemOutput(payload = {}) {
62
- const source = normalizeObjectInput(payload);
63
-
64
- return {
65
- id: normalizeRecordId(source.id, { fallback: "" }),
66
- slug: normalizeLowerText(source.slug),
67
- name: normalizeText(source.name),
68
- avatarUrl: normalizeText(source.avatarUrl),
69
- roleSid: normalizeLowerText(source.roleSid || "member") || "member",
70
- isAccessible: source.isAccessible !== false
71
- };
72
- }
73
-
74
- const responseRecordSchema = Type.Object(
75
- {
76
- id: recordIdSchema,
77
- slug: Type.String({ minLength: 1 }),
78
- name: Type.String({ minLength: 1, maxLength: 160 }),
79
- ownerUserId: recordIdSchema,
80
- avatarUrl: Type.String()
81
- },
82
- { additionalProperties: false }
83
- );
84
-
85
- const listItemSchema = Type.Object(
86
- {
87
- id: recordIdSchema,
88
- slug: Type.String({ minLength: 1 }),
89
- name: Type.String({ minLength: 1, maxLength: 160 }),
90
- avatarUrl: Type.String(),
91
- roleSid: Type.String({ minLength: 1 }),
92
- isAccessible: Type.Boolean()
93
- },
94
- { additionalProperties: false }
95
- );
96
-
97
- const createRequestBodySchema = Type.Object(
98
- {
99
- name: Type.String({ minLength: 1, maxLength: 160 }),
100
- slug: Type.Optional(Type.String({ minLength: 1, maxLength: 120 })),
101
- ownerUserId: Type.Optional(recordIdInputSchema)
102
- },
103
- { additionalProperties: false }
104
- );
22
+ const workspaceOutputSchema = createSchema({
23
+ id: { type: "string", required: true, minLength: 1, pattern: RECORD_ID_PATTERN },
24
+ slug: { type: "string", required: true, minLength: 1, maxLength: 120 },
25
+ name: { type: "string", required: true, minLength: 1, maxLength: 160 },
26
+ ownerUserId: { type: "string", required: true, minLength: 1, pattern: RECORD_ID_PATTERN },
27
+ avatarUrl: { type: "string", required: true }
28
+ });
105
29
 
106
- const patchRequestBodySchema = Type.Object(
107
- {
108
- name: Type.Optional(Type.String({ minLength: 1, maxLength: 160 })),
109
- avatarUrl: Type.Optional(
110
- Type.String({
111
- pattern: "^(https?://.+)?$",
112
- messages: {
113
- pattern: "Workspace avatar URL must be a valid absolute URL (http:// or https://).",
114
- default: "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
115
- }
116
- })
117
- )
118
- },
119
- { additionalProperties: false }
120
- );
30
+ const workspaceListItemSchema = createSchema({
31
+ id: { type: "string", required: true, minLength: 1, pattern: RECORD_ID_PATTERN },
32
+ slug: { type: "string", required: true, minLength: 1, maxLength: 120 },
33
+ name: { type: "string", required: true, minLength: 1, maxLength: 160 },
34
+ avatarUrl: { type: "string", required: true },
35
+ roleSid: { type: "string", required: true, minLength: 1, maxLength: 64 },
36
+ isAccessible: { type: "boolean", required: true }
37
+ });
121
38
 
122
- const responseRecordValidator = Object.freeze({
123
- schema: responseRecordSchema,
124
- normalize: normalizeWorkspaceOutput
39
+ const workspaceCreateBodySchema = createSchema({
40
+ name: { type: "string", required: true, minLength: 1, maxLength: 160 },
41
+ slug: { type: "string", required: false, lowercase: true, minLength: 1, maxLength: 120 },
42
+ ownerUserId: { type: "id", required: false }
125
43
  });
126
44
 
127
- const workspaceSummaryOutputValidator = Object.freeze({
128
- schema: listItemSchema,
129
- normalize: normalizeWorkspaceListItemOutput
45
+ const workspacePatchBodySchema = createSchema({
46
+ name: { type: "string", required: false, minLength: 1, maxLength: 160 },
47
+ avatarUrl: {
48
+ type: "string",
49
+ required: false,
50
+ pattern: "^(https?://.+)?$",
51
+ messages: {
52
+ pattern: "Workspace avatar URL must be a valid absolute URL (http:// or https://).",
53
+ default: "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
54
+ }
55
+ }
130
56
  });
131
57
 
132
- const resource = {
58
+ const workspaceListItemOutputValidator = createSchemaDefinition(workspaceListItemSchema, "replace");
59
+
60
+ const resource = defineCrudResource({
133
61
  namespace: "workspace",
134
- messages: {
135
- validation: "Fix invalid workspace values and try again.",
136
- saveSuccess: "Workspace updated.",
137
- saveError: "Unable to update workspace.",
138
- apiValidation: "Validation failed."
62
+ tableName: "workspaces",
63
+ searchSchema: {
64
+ id: { type: "id", actualField: "id" }
139
65
  },
140
- operations: {
141
- view: {
142
- method: "GET",
143
- outputValidator: responseRecordValidator
66
+ schema: {
67
+ slug: {
68
+ type: "string",
69
+ required: true,
70
+ max: 120,
71
+ search: true,
72
+ setter: (value) => normalizeLowerText(value)
73
+ },
74
+ name: {
75
+ type: "string",
76
+ required: true,
77
+ max: 160,
78
+ search: true,
79
+ setter: (value) => normalizeText(value)
144
80
  },
145
- list: {
146
- method: "GET",
147
- outputValidator: createCursorListValidator(workspaceSummaryOutputValidator)
81
+ ownerUserId: {
82
+ type: "id",
83
+ required: true,
84
+ search: true,
85
+ belongsTo: "userProfiles",
86
+ as: "owner",
87
+ storage: { column: "owner_user_id" }
148
88
  },
149
- create: {
150
- method: "POST",
151
- bodyValidator: {
152
- schema: createRequestBodySchema,
153
- normalize: normalizeWorkspaceInput
154
- },
155
- outputValidator: responseRecordValidator
89
+ isPersonal: {
90
+ type: "boolean",
91
+ defaultTo: false,
92
+ search: true,
93
+ storage: { column: "is_personal" }
156
94
  },
157
- replace: {
158
- method: "PUT",
159
- bodyValidator: {
160
- schema: createRequestBodySchema,
161
- normalize: normalizeWorkspaceInput
162
- },
163
- outputValidator: responseRecordValidator
95
+ avatarUrl: {
96
+ type: "string",
97
+ max: 512,
98
+ defaultTo: "",
99
+ storage: { column: "avatar_url" },
100
+ setter: (value) => normalizeWorkspaceAvatarUrl(value)
164
101
  },
165
- patch: {
166
- method: "PATCH",
167
- bodyValidator: {
168
- schema: patchRequestBodySchema,
169
- normalize: normalizeWorkspaceInput
170
- },
171
- outputValidator: responseRecordValidator
102
+ createdAt: {
103
+ type: "dateTime",
104
+ default: "now()",
105
+ storage: {
106
+ column: "created_at",
107
+ writeSerializer: "datetime-utc"
108
+ }
109
+ },
110
+ updatedAt: {
111
+ type: "dateTime",
112
+ default: "now()",
113
+ storage: {
114
+ column: "updated_at",
115
+ writeSerializer: "datetime-utc"
116
+ }
117
+ },
118
+ deletedAt: {
119
+ type: "dateTime",
120
+ nullable: true,
121
+ search: true,
122
+ storage: {
123
+ column: "deleted_at",
124
+ writeSerializer: "datetime-utc"
125
+ }
172
126
  }
127
+ },
128
+ messages: {
129
+ validation: "Fix invalid workspace values and try again.",
130
+ saveSuccess: "Workspace updated.",
131
+ saveError: "Unable to update workspace.",
132
+ apiValidation: "Validation failed."
133
+ },
134
+ crudOperations: ["view", "list", "create", "replace", "patch"],
135
+ crud: {
136
+ output: workspaceOutputSchema,
137
+ listItemOutput: workspaceListItemOutputValidator,
138
+ createBody: workspaceCreateBodySchema,
139
+ replaceBody: workspaceCreateBodySchema,
140
+ patchBody: workspacePatchBodySchema
173
141
  }
174
- };
142
+ });
175
143
 
176
144
  export { resource as workspaceResource };
145
+ export { workspaceListItemOutputValidator };
@@ -0,0 +1,31 @@
1
+ import { createSchema } from "json-rest-schema";
2
+
3
+ const workspaceRoleDescriptorSchema = createSchema({
4
+ id: { type: "string", required: true, minLength: 1, maxLength: 64 },
5
+ assignable: { type: "boolean", required: true },
6
+ permissions: {
7
+ type: "array",
8
+ required: true,
9
+ items: { type: "string", minLength: 1 }
10
+ }
11
+ });
12
+
13
+ const workspaceRoleCatalogSchema = createSchema({
14
+ collaborationEnabled: { type: "boolean", required: true },
15
+ defaultInviteRole: { type: "string", required: true, minLength: 0, maxLength: 64 },
16
+ roles: {
17
+ type: "array",
18
+ required: true,
19
+ items: workspaceRoleDescriptorSchema
20
+ },
21
+ assignableRoleIds: {
22
+ type: "array",
23
+ required: true,
24
+ items: { type: "string", minLength: 1, maxLength: 64 }
25
+ }
26
+ });
27
+
28
+ export {
29
+ workspaceRoleCatalogSchema,
30
+ workspaceRoleDescriptorSchema
31
+ };