@jskit-ai/workspaces-core 0.1.21 → 0.1.23

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 (30) hide show
  1. package/package.descriptor.mjs +2 -2
  2. package/package.json +6 -6
  3. package/src/server/common/repositories/workspaceInvitesRepository.js +68 -65
  4. package/src/server/common/repositories/workspaceMembershipsRepository.js +83 -52
  5. package/src/server/common/repositories/workspacesRepository.js +42 -67
  6. package/src/server/common/resources/workspaceInvitesResource.js +207 -0
  7. package/src/server/common/resources/workspaceMembershipsResource.js +154 -0
  8. package/src/server/common/resources/workspacesResource.js +170 -0
  9. package/src/server/registerWorkspaceBootstrap.js +1 -1
  10. package/src/server/registerWorkspaceCore.js +3 -3
  11. package/src/server/registerWorkspaceRepositories.js +3 -3
  12. package/src/server/workspaceBootstrapContributor.js +4 -4
  13. package/src/server/workspaceMembers/registerWorkspaceMembers.js +2 -2
  14. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +2 -2
  15. package/src/server/workspaceSettings/registerWorkspaceSettings.js +2 -2
  16. package/src/server/workspaceSettings/workspaceSettingsRepository.js +5 -4
  17. package/src/shared/resources/workspaceMembersResource.js +1 -1
  18. package/src/shared/resources/workspacePendingInvitationsResource.js +1 -1
  19. package/src/shared/resources/workspaceResource.js +1 -1
  20. package/src/shared/resources/workspaceSettingsFields.js +10 -4
  21. package/src/shared/resources/workspaceSettingsResource.js +1 -1
  22. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +9 -9
  23. package/test/exportsContract.test.js +3 -1
  24. package/test/registerWorkspaceBootstrap.test.js +1 -1
  25. package/test/registerWorkspaceSettings.test.js +1 -1
  26. package/test/usersRouteResources.test.js +1 -1
  27. package/test/workspaceBootstrapContributor.test.js +3 -3
  28. package/test/workspaceInvitesRepository.test.js +129 -18
  29. package/test/workspaceMembershipsRepository.test.js +212 -0
  30. package/test/workspacesRepository.test.js +253 -0
@@ -68,7 +68,7 @@ const pendingInvitationsListOutputValidator = Object.freeze({
68
68
  const WORKSPACE_PENDING_INVITATIONS_MESSAGES = createOperationMessages();
69
69
 
70
70
  const workspacePendingInvitationsResource = Object.freeze({
71
- resource: "workspacePendingInvitations",
71
+ namespace: "workspacePendingInvitations",
72
72
  messages: WORKSPACE_PENDING_INVITATIONS_MESSAGES,
73
73
  operations: Object.freeze({
74
74
  list: Object.freeze({
@@ -130,7 +130,7 @@ const workspaceSummaryOutputValidator = Object.freeze({
130
130
  });
131
131
 
132
132
  const resource = {
133
- resource: "workspace",
133
+ namespace: "workspace",
134
134
  messages: {
135
135
  validation: "Fix invalid workspace values and try again.",
136
136
  saveSuccess: "Workspace updated.",
@@ -17,9 +17,13 @@ function defineField(field = {}) {
17
17
  if (!field.outputSchema || typeof field.outputSchema !== "object") {
18
18
  throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires outputSchema.`);
19
19
  }
20
- const dbColumn = normalizeText(field.dbColumn);
21
- if (!dbColumn) {
22
- throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires dbColumn.`);
20
+ const repository = field?.repository;
21
+ if (!repository || typeof repository !== "object" || Array.isArray(repository)) {
22
+ throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires repository.column.`);
23
+ }
24
+ const repositoryColumn = normalizeText(repository.column);
25
+ if (!repositoryColumn) {
26
+ throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires repository.column.`);
23
27
  }
24
28
  if (typeof field.normalizeInput !== "function") {
25
29
  throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires normalizeInput.`);
@@ -33,7 +37,9 @@ function defineField(field = {}) {
33
37
 
34
38
  workspaceSettingsFields.push({
35
39
  key,
36
- dbColumn,
40
+ repository: Object.freeze({
41
+ column: repositoryColumn
42
+ }),
37
43
  required: field.required !== false,
38
44
  inputSchema: field.inputSchema,
39
45
  outputSchema: field.outputSchema,
@@ -117,7 +117,7 @@ const responseRecordValidator = Object.freeze({
117
117
  });
118
118
 
119
119
  const resource = {
120
- resource: "workspaceSettings",
120
+ namespace: "workspaceSettings",
121
121
  messages: {
122
122
  validation: "Fix invalid workspace settings values and try again.",
123
123
  saveSuccess: "Workspace settings updated.",
@@ -22,7 +22,7 @@ resetWorkspaceSettingsFields();
22
22
 
23
23
  defineField({
24
24
  key: "lightPrimaryColor",
25
- dbColumn: "light_primary_color",
25
+ repository: { column: "light_primary_color" },
26
26
  required: true,
27
27
  inputSchema: Type.String({
28
28
  minLength: 7,
@@ -42,7 +42,7 @@ defineField({
42
42
 
43
43
  defineField({
44
44
  key: "lightSecondaryColor",
45
- dbColumn: "light_secondary_color",
45
+ repository: { column: "light_secondary_color" },
46
46
  required: true,
47
47
  inputSchema: Type.String({
48
48
  minLength: 7,
@@ -62,7 +62,7 @@ defineField({
62
62
 
63
63
  defineField({
64
64
  key: "lightSurfaceColor",
65
- dbColumn: "light_surface_color",
65
+ repository: { column: "light_surface_color" },
66
66
  required: true,
67
67
  inputSchema: Type.String({
68
68
  minLength: 7,
@@ -82,7 +82,7 @@ defineField({
82
82
 
83
83
  defineField({
84
84
  key: "lightSurfaceVariantColor",
85
- dbColumn: "light_surface_variant_color",
85
+ repository: { column: "light_surface_variant_color" },
86
86
  required: true,
87
87
  inputSchema: Type.String({
88
88
  minLength: 7,
@@ -102,7 +102,7 @@ defineField({
102
102
 
103
103
  defineField({
104
104
  key: "darkPrimaryColor",
105
- dbColumn: "dark_primary_color",
105
+ repository: { column: "dark_primary_color" },
106
106
  required: true,
107
107
  inputSchema: Type.String({
108
108
  minLength: 7,
@@ -122,7 +122,7 @@ defineField({
122
122
 
123
123
  defineField({
124
124
  key: "darkSecondaryColor",
125
- dbColumn: "dark_secondary_color",
125
+ repository: { column: "dark_secondary_color" },
126
126
  required: true,
127
127
  inputSchema: Type.String({
128
128
  minLength: 7,
@@ -142,7 +142,7 @@ defineField({
142
142
 
143
143
  defineField({
144
144
  key: "darkSurfaceColor",
145
- dbColumn: "dark_surface_color",
145
+ repository: { column: "dark_surface_color" },
146
146
  required: true,
147
147
  inputSchema: Type.String({
148
148
  minLength: 7,
@@ -162,7 +162,7 @@ defineField({
162
162
 
163
163
  defineField({
164
164
  key: "darkSurfaceVariantColor",
165
- dbColumn: "dark_surface_variant_color",
165
+ repository: { column: "dark_surface_variant_color" },
166
166
  required: true,
167
167
  inputSchema: Type.String({
168
168
  minLength: 7,
@@ -182,7 +182,7 @@ defineField({
182
182
 
183
183
  defineField({
184
184
  key: "invitesEnabled",
185
- dbColumn: "invites_enabled",
185
+ repository: { column: "invites_enabled" },
186
186
  required: true,
187
187
  inputSchema: Type.Boolean({
188
188
  messages: {
@@ -14,7 +14,9 @@ test("workspaces-core exports are explicit and aligned with production usage", (
14
14
  packageDir: PACKAGE_DIR,
15
15
  packageId: "@jskit-ai/workspaces-core",
16
16
  requiredExports: [
17
- "./server/WorkspacesCoreServiceProvider"
17
+ "./server/WorkspacesCoreServiceProvider",
18
+ "./server/validators/routeParamsValidator",
19
+ "./server/support/workspaceRouteInput"
18
20
  ]
19
21
  });
20
22
 
@@ -55,7 +55,7 @@ test("registerWorkspaceBootstrap resolves the canonical pending invitations serv
55
55
  listPendingInvitesForUser() {}
56
56
  };
57
57
  }
58
- if (token === "usersRepository") {
58
+ if (token === "internal.repository.user-profiles") {
59
59
  return {
60
60
  findById() {}
61
61
  };
@@ -28,7 +28,7 @@ test("registerWorkspaceSettings registers workspace settings service realtime ev
28
28
 
29
29
  registerWorkspaceSettings(app);
30
30
 
31
- assert.equal(singletonBindings.has("workspaceSettingsRepository"), true);
31
+ assert.equal(singletonBindings.has("internal.repository.workspace-settings"), true);
32
32
  assert.equal(serviceCalls.length, 1);
33
33
  assert.equal(serviceCalls[0].token, "workspaces.settings.service");
34
34
  assert.equal(typeof serviceCalls[0].factory, "function");
@@ -12,7 +12,7 @@ import { workspaceSettingsResource } from "../src/shared/resources/workspaceSett
12
12
  function assertResourceShape(resource, label) {
13
13
  assert.ok(resource, `${label} resource must exist.`);
14
14
  assert.equal(typeof resource, "object", `${label} resource must be an object.`);
15
- assert.equal(typeof resource.resource, "string", `${label}.resource must be a string.`);
15
+ assert.equal(typeof resource.namespace, "string", `.namespace must be a string.`);
16
16
 
17
17
  for (const operationName of ["view", "list", "create", "replace", "patch"]) {
18
18
  const operation = resource.operations?.[operationName];
@@ -35,7 +35,7 @@ test("workspace bootstrap contributor passes actor context to pending invites se
35
35
  return [];
36
36
  }
37
37
  },
38
- usersRepository: {
38
+ userProfilesRepository: {
39
39
  async findById() {
40
40
  return profile;
41
41
  }
@@ -78,7 +78,7 @@ test("workspace bootstrap contributor resolves workspace slug from bootstrap que
78
78
  return [];
79
79
  }
80
80
  },
81
- usersRepository: {
81
+ userProfilesRepository: {
82
82
  async findById() {
83
83
  return profile;
84
84
  }
@@ -123,7 +123,7 @@ test("workspace bootstrap contributor reports unauthenticated requested workspac
123
123
  assert.fail("listPendingInvitesForUser should not run for unauthenticated payloads");
124
124
  }
125
125
  },
126
- usersRepository: {
126
+ userProfilesRepository: {
127
127
  async findById() {
128
128
  assert.fail("findById should not run for unauthenticated payloads");
129
129
  }
@@ -1,13 +1,19 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
+ import { toIsoString, toNullableDateTime } from "@jskit-ai/database-runtime/shared";
3
4
  import { createRepository } from "../src/server/common/repositories/workspaceInvitesRepository.js";
4
5
 
5
- function createKnexStub() {
6
+ function createKnexStub({
7
+ rowById = new Map(),
8
+ pendingRow = null,
9
+ joinedRows = []
10
+ } = {}) {
6
11
  const state = {
7
- insertPayload: null
12
+ insertPayload: null,
13
+ updatePayload: null
8
14
  };
9
15
 
10
- const row = {
16
+ const defaultRow = pendingRow || {
11
17
  id: 1,
12
18
  workspace_id: 1,
13
19
  email: "invitee@example.com",
@@ -23,28 +29,54 @@ function createKnexStub() {
23
29
  };
24
30
 
25
31
  function tableBuilder(tableName) {
32
+ if (tableName === "workspace_invites as wi") {
33
+ return {
34
+ join() {
35
+ return this;
36
+ },
37
+ where() {
38
+ return this;
39
+ },
40
+ orderBy() {
41
+ return this;
42
+ },
43
+ select() {
44
+ return Promise.resolve([...joinedRows]);
45
+ }
46
+ };
47
+ }
48
+
26
49
  assert.equal(tableName, "workspace_invites");
27
- return {
50
+ const query = {
51
+ criteriaList: [],
52
+ select() {
53
+ return this;
54
+ },
28
55
  insert(payload) {
29
56
  state.insertPayload = payload;
30
57
  return Promise.resolve([1]);
31
58
  },
32
59
  where(criteria) {
33
- assert.equal(typeof criteria, "object");
34
- return {
35
- first() {
36
- return Promise.resolve({ ...row });
37
- },
38
- orderBy() {
39
- return {
40
- first() {
41
- return Promise.resolve({ ...row });
42
- }
43
- };
44
- }
45
- };
60
+ this.criteriaList.push(criteria);
61
+ return this;
62
+ },
63
+ orderBy() {
64
+ return this;
65
+ },
66
+ update(payload) {
67
+ state.updatePayload = payload;
68
+ return Promise.resolve(1);
69
+ },
70
+ first() {
71
+ const criteria = Object.assign({}, ...this.criteriaList);
72
+ if (Object.hasOwn(criteria, "id")) {
73
+ return Promise.resolve(rowById.get(String(criteria.id)) || null);
74
+ }
75
+ return Promise.resolve({ ...defaultRow });
46
76
  }
47
77
  };
78
+
79
+ return query;
48
80
  }
49
81
 
50
82
  return { knexStub: tableBuilder, state };
@@ -64,7 +96,7 @@ test("workspaceInvitesRepository.insert normalizes expiresAt ISO input to databa
64
96
  expiresAt: "2026-03-16T00:26:35.709Z"
65
97
  });
66
98
 
67
- assert.equal(state.insertPayload.expires_at, "2026-03-16 00:26:35.709");
99
+ assert.equal(state.insertPayload.expires_at, toNullableDateTime("2026-03-16T00:26:35.709Z"));
68
100
  });
69
101
 
70
102
  test("workspaceInvitesRepository.findPendingByTokenHash reads from invites table without workspace join", async () => {
@@ -90,6 +122,9 @@ test("workspaceInvitesRepository.findPendingByTokenHash reads from invites table
90
122
  const repository = createRepository((tableName) => {
91
123
  calls.tableName = String(tableName || "");
92
124
  return {
125
+ select() {
126
+ return this;
127
+ },
93
128
  where(criteria) {
94
129
  calls.whereCriteria = criteria;
95
130
  return this;
@@ -109,3 +144,79 @@ test("workspaceInvitesRepository.findPendingByTokenHash reads from invites table
109
144
  assert.equal(invite?.workspaceId, "9");
110
145
  assert.equal(invite?.workspaceSlug, undefined);
111
146
  });
147
+
148
+ test("workspaceInvitesRepository.markAcceptedById uses the internal invite resource for normalized patch writes", async () => {
149
+ const acceptedRow = {
150
+ id: 1,
151
+ workspace_id: 1,
152
+ email: "invitee@example.com",
153
+ role_sid: "member",
154
+ status: "accepted",
155
+ token_hash: "hash",
156
+ invited_by_user_id: 1,
157
+ expires_at: "2026-03-16 00:26:35.709",
158
+ accepted_at: "2026-03-10 00:26:35.710",
159
+ revoked_at: null,
160
+ created_at: "2026-03-09 00:26:35.710",
161
+ updated_at: "2026-03-10 00:26:35.710"
162
+ };
163
+ const { knexStub, state } = createKnexStub({
164
+ rowById: new Map([["1", acceptedRow]])
165
+ });
166
+ const repository = createRepository(knexStub);
167
+
168
+ await repository.markAcceptedById("1");
169
+
170
+ assert.equal(state.updatePayload.status, "accepted");
171
+ assert.equal(typeof state.updatePayload.accepted_at, "string");
172
+ assert.match(state.updatePayload.accepted_at, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/);
173
+ assert.equal(typeof state.updatePayload.updated_at, "string");
174
+ assert.match(state.updatePayload.updated_at, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/);
175
+ });
176
+
177
+ test("workspaceInvitesRepository.listPendingByWorkspaceIdWithWorkspace keeps workspace join fields outside the base resource contract", async () => {
178
+ const { knexStub } = createKnexStub({
179
+ joinedRows: [
180
+ {
181
+ id: 1,
182
+ workspace_id: 9,
183
+ email: "invitee@example.com",
184
+ role_sid: "member",
185
+ status: "pending",
186
+ token_hash: "hash-token",
187
+ invited_by_user_id: 1,
188
+ expires_at: "2030-01-01 00:00:00.000",
189
+ accepted_at: null,
190
+ revoked_at: null,
191
+ created_at: "2026-03-09 00:26:35.710",
192
+ updated_at: "2026-03-09 00:26:35.710",
193
+ workspace_slug: "tonymobily3",
194
+ workspace_name: "TonyMobily3",
195
+ workspace_avatar_url: "https://example.com/avatar.png"
196
+ }
197
+ ]
198
+ });
199
+ const repository = createRepository(knexStub);
200
+
201
+ const invites = await repository.listPendingByWorkspaceIdWithWorkspace("9");
202
+
203
+ assert.deepEqual(invites, [
204
+ {
205
+ id: "1",
206
+ workspaceId: "9",
207
+ email: "invitee@example.com",
208
+ roleSid: "member",
209
+ status: "pending",
210
+ tokenHash: "hash-token",
211
+ invitedByUserId: "1",
212
+ expiresAt: toIsoString("2030-01-01 00:00:00.000"),
213
+ acceptedAt: null,
214
+ revokedAt: null,
215
+ createdAt: toIsoString("2026-03-09 00:26:35.710"),
216
+ updatedAt: toIsoString("2026-03-09 00:26:35.710"),
217
+ workspaceSlug: "tonymobily3",
218
+ workspaceName: "TonyMobily3",
219
+ workspaceAvatarUrl: "https://example.com/avatar.png"
220
+ }
221
+ ]);
222
+ });
@@ -0,0 +1,212 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { toIsoString } from "@jskit-ai/database-runtime/shared";
4
+ import { createRepository } from "../src/server/common/repositories/workspaceMembershipsRepository.js";
5
+
6
+ function createKnexStub({
7
+ rowById = new Map(),
8
+ rowByComposite = new Map(),
9
+ memberSummaryRows = []
10
+ } = {}) {
11
+ const state = {
12
+ insertPayload: null,
13
+ updatePayload: null
14
+ };
15
+
16
+ function buildMembershipsQuery(tableName) {
17
+ if (tableName === "workspace_memberships as wm") {
18
+ return {
19
+ join() {
20
+ return this;
21
+ },
22
+ where() {
23
+ return this;
24
+ },
25
+ orderBy() {
26
+ return this;
27
+ },
28
+ select() {
29
+ return Promise.resolve([...memberSummaryRows]);
30
+ }
31
+ };
32
+ }
33
+
34
+ const query = {
35
+ criteriaList: [],
36
+ select() {
37
+ return this;
38
+ },
39
+ insert(payload) {
40
+ state.insertPayload = payload;
41
+ const insertedRow = rowById.get("1");
42
+ if (insertedRow) {
43
+ rowByComposite.set(`${payload.workspace_id}:${payload.user_id}`, insertedRow);
44
+ }
45
+ return Promise.resolve([1]);
46
+ },
47
+ where(criteria) {
48
+ this.criteriaList.push(criteria);
49
+ return this;
50
+ },
51
+ update(payload) {
52
+ state.updatePayload = payload;
53
+ const criteria = Object.assign({}, ...this.criteriaList);
54
+ const existingRow = rowById.get(String(criteria.id));
55
+ if (existingRow) {
56
+ const updatedRow = {
57
+ ...existingRow,
58
+ ...payload
59
+ };
60
+ rowById.set(String(criteria.id), updatedRow);
61
+ rowByComposite.set(`${updatedRow.workspace_id}:${updatedRow.user_id}`, updatedRow);
62
+ }
63
+ return Promise.resolve(1);
64
+ },
65
+ first() {
66
+ const criteria = Object.assign({}, ...this.criteriaList);
67
+ if (Object.hasOwn(criteria, "id")) {
68
+ return Promise.resolve(rowById.get(String(criteria.id)) || null);
69
+ }
70
+ if (Object.hasOwn(criteria, "workspace_id") && Object.hasOwn(criteria, "user_id")) {
71
+ return Promise.resolve(
72
+ rowByComposite.get(`${criteria.workspace_id}:${criteria.user_id}`) || null
73
+ );
74
+ }
75
+ return Promise.resolve(null);
76
+ }
77
+ };
78
+
79
+ return query;
80
+ }
81
+
82
+ function knex(tableName) {
83
+ if (tableName === "workspace_memberships" || tableName === "workspace_memberships as wm") {
84
+ return buildMembershipsQuery(tableName);
85
+ }
86
+ throw new Error(`Unexpected table ${tableName}`);
87
+ }
88
+
89
+ knex.transaction = async (work) => work(knex);
90
+
91
+ return { knex, state };
92
+ }
93
+
94
+ test("workspaceMembershipsRepository.findByWorkspaceIdAndUserId normalizes canonical membership rows via the internal resource", async () => {
95
+ const membershipRow = {
96
+ id: 11,
97
+ workspace_id: 7,
98
+ user_id: 9,
99
+ role_sid: "owner",
100
+ status: "active",
101
+ created_at: "2026-03-09 00:26:35.710",
102
+ updated_at: "2026-03-10 00:26:35.710"
103
+ };
104
+ const { knex } = createKnexStub({
105
+ rowByComposite: new Map([["7:9", membershipRow]])
106
+ });
107
+ const repository = createRepository(knex);
108
+
109
+ const membership = await repository.findByWorkspaceIdAndUserId("7", "9");
110
+
111
+ assert.deepEqual(membership, {
112
+ id: "11",
113
+ workspaceId: "7",
114
+ userId: "9",
115
+ roleSid: "owner",
116
+ status: "active",
117
+ createdAt: toIsoString("2026-03-09 00:26:35.710"),
118
+ updatedAt: toIsoString("2026-03-10 00:26:35.710")
119
+ });
120
+ });
121
+
122
+ test("workspaceMembershipsRepository.ensureOwnerMembership upgrades an existing membership through the runtime update path", async () => {
123
+ const existingRow = {
124
+ id: 11,
125
+ workspace_id: 7,
126
+ user_id: 9,
127
+ role_sid: "member",
128
+ status: "pending",
129
+ created_at: "2026-03-09 00:26:35.710",
130
+ updated_at: "2026-03-09 00:26:35.710"
131
+ };
132
+ const refreshedRow = {
133
+ ...existingRow,
134
+ role_sid: "owner",
135
+ status: "active",
136
+ updated_at: "2026-03-10 00:26:35.710"
137
+ };
138
+ const { knex, state } = createKnexStub({
139
+ rowById: new Map([["11", refreshedRow]]),
140
+ rowByComposite: new Map([["7:9", existingRow]])
141
+ });
142
+ const repository = createRepository(knex);
143
+
144
+ const membership = await repository.ensureOwnerMembership("7", "9");
145
+
146
+ assert.equal(state.updatePayload.role_sid, "owner");
147
+ assert.equal(state.updatePayload.status, "active");
148
+ assert.equal(typeof state.updatePayload.updated_at, "string");
149
+ assert.deepEqual(membership, {
150
+ id: "11",
151
+ workspaceId: "7",
152
+ userId: "9",
153
+ roleSid: "owner",
154
+ status: "active",
155
+ createdAt: toIsoString("2026-03-09 00:26:35.710"),
156
+ updatedAt: toIsoString(state.updatePayload.updated_at)
157
+ });
158
+ });
159
+
160
+ test("workspaceMembershipsRepository.upsertMembership creates normalized memberships through the runtime create path", async () => {
161
+ const createdRow = {
162
+ id: 1,
163
+ workspace_id: 7,
164
+ user_id: 9,
165
+ role_sid: "admin",
166
+ status: "active",
167
+ created_at: "2026-03-09 00:26:35.710",
168
+ updated_at: "2026-03-09 00:26:35.710"
169
+ };
170
+ const { knex, state } = createKnexStub({
171
+ rowById: new Map([["1", createdRow]]),
172
+ rowByComposite: new Map()
173
+ });
174
+ const repository = createRepository(knex);
175
+
176
+ await repository.upsertMembership("7", "9", {
177
+ roleSid: "ADMIN",
178
+ status: "ACTIVE"
179
+ });
180
+
181
+ assert.equal(state.insertPayload.workspace_id, "7");
182
+ assert.equal(state.insertPayload.user_id, "9");
183
+ assert.equal(state.insertPayload.role_sid, "admin");
184
+ assert.equal(state.insertPayload.status, "active");
185
+ });
186
+
187
+ test("workspaceMembershipsRepository.listActiveByWorkspaceId keeps summary rows separate from the canonical membership resource", async () => {
188
+ const { knex } = createKnexStub({
189
+ memberSummaryRows: [
190
+ {
191
+ user_id: 9,
192
+ role_sid: "owner",
193
+ status: "active",
194
+ display_name: "Chiara",
195
+ email: "CHIARA@example.com"
196
+ }
197
+ ]
198
+ });
199
+ const repository = createRepository(knex);
200
+
201
+ const members = await repository.listActiveByWorkspaceId("7");
202
+
203
+ assert.deepEqual(members, [
204
+ {
205
+ userId: "9",
206
+ roleSid: "owner",
207
+ status: "active",
208
+ displayName: "Chiara",
209
+ email: "chiara@example.com"
210
+ }
211
+ ]);
212
+ });