@jskit-ai/workspaces-core 0.1.33 → 0.1.35

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/workspaces-core",
4
- version: "0.1.33",
4
+ version: "0.1.35",
5
5
  kind: "runtime",
6
6
  description: "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
7
7
  dependsOn: [
@@ -116,10 +116,10 @@ export default Object.freeze({
116
116
  mutations: {
117
117
  dependencies: {
118
118
  runtime: {
119
- "@jskit-ai/json-rest-api-core": "0.1.2",
120
- "@jskit-ai/resource-core": "0.1.2",
121
- "@jskit-ai/resource-crud-core": "0.1.2",
122
- "@jskit-ai/users-core": "0.1.67"
119
+ "@jskit-ai/json-rest-api-core": "0.1.4",
120
+ "@jskit-ai/resource-core": "0.1.4",
121
+ "@jskit-ai/resource-crud-core": "0.1.4",
122
+ "@jskit-ai/users-core": "0.1.69"
123
123
  },
124
124
  dev: {}
125
125
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/workspaces-core",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -17,14 +17,14 @@
17
17
  "./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js"
18
18
  },
19
19
  "dependencies": {
20
- "@jskit-ai/auth-core": "0.1.56",
21
- "@jskit-ai/database-runtime": "0.1.57",
22
- "@jskit-ai/http-runtime": "0.1.56",
23
- "@jskit-ai/json-rest-api-core": "0.1.2",
24
- "@jskit-ai/kernel": "0.1.57",
25
- "@jskit-ai/resource-crud-core": "0.1.2",
26
- "@jskit-ai/resource-core": "0.1.2",
27
- "@jskit-ai/users-core": "0.1.67",
20
+ "@jskit-ai/auth-core": "0.1.58",
21
+ "@jskit-ai/database-runtime": "0.1.59",
22
+ "@jskit-ai/http-runtime": "0.1.58",
23
+ "@jskit-ai/json-rest-api-core": "0.1.4",
24
+ "@jskit-ai/kernel": "0.1.59",
25
+ "@jskit-ai/resource-crud-core": "0.1.4",
26
+ "@jskit-ai/resource-core": "0.1.4",
27
+ "@jskit-ai/users-core": "0.1.69",
28
28
  "json-rest-schema": "1.x.x"
29
29
  }
30
30
  }
@@ -12,7 +12,7 @@ import {
12
12
  createJsonApiInputRecord,
13
13
  createJsonApiRelationship,
14
14
  createJsonRestContext,
15
- simplifyJsonApiDocument
15
+ extractJsonRestCollectionRows
16
16
  } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
17
17
 
18
18
  const RESOURCE_TYPE = "workspaceInvites";
@@ -105,19 +105,19 @@ function createRepository({ api, knex } = {}) {
105
105
  const withTransaction = createWithTransaction(knex);
106
106
 
107
107
  async function queryInvites(filters = {}, options = {}, { includeWorkspace = false } = {}) {
108
- const result = await api.resources.workspaceInvites.query(
109
- {
110
- queryParams: {
111
- filters,
112
- ...(includeWorkspace ? { include: ["workspace"] } : {})
108
+ return extractJsonRestCollectionRows(
109
+ await api.resources.workspaceInvites.query(
110
+ {
111
+ queryParams: {
112
+ filters,
113
+ ...(includeWorkspace ? { include: ["workspace"] } : {})
114
+ },
115
+ transaction: options?.trx || null,
116
+ simplified: true
113
117
  },
114
- transaction: options?.trx || null,
115
- simplified: false
116
- },
117
- createJsonRestContext(options?.context || null)
118
+ createJsonRestContext(options?.context || null)
119
+ )
118
120
  );
119
-
120
- return Array.isArray(simplifyJsonApiDocument(result)) ? simplifyJsonApiDocument(result) : [];
121
121
  }
122
122
 
123
123
  async function findPendingByTokenHash(tokenHash, options = {}) {
@@ -213,13 +213,12 @@ function createRepository({ api, knex } = {}) {
213
213
  })
214
214
  }
215
215
  ),
216
- transaction: options?.trx || null,
217
- simplified: false
216
+ transaction: options?.trx || null
218
217
  },
219
218
  createJsonRestContext(options?.context || null)
220
219
  );
221
220
 
222
- return normalizeInviteRecord(simplifyJsonApiDocument(created));
221
+ return normalizeInviteRecord(created);
223
222
  } catch (error) {
224
223
  if (!isDuplicateEntryError(error)) {
225
224
  throw error;
@@ -271,8 +270,7 @@ function createRepository({ api, knex } = {}) {
271
270
  id: row.id
272
271
  }
273
272
  ),
274
- transaction: options?.trx || null,
275
- simplified: false
273
+ transaction: options?.trx || null
276
274
  },
277
275
  createJsonRestContext(options?.context || null)
278
276
  );
@@ -298,8 +296,7 @@ function createRepository({ api, knex } = {}) {
298
296
  id: normalizedInviteId
299
297
  }
300
298
  ),
301
- transaction: options?.trx || null,
302
- simplified: false
299
+ transaction: options?.trx || null
303
300
  },
304
301
  createJsonRestContext(options?.context || null)
305
302
  );
@@ -324,8 +321,7 @@ function createRepository({ api, knex } = {}) {
324
321
  id: normalizedInviteId
325
322
  }
326
323
  ),
327
- transaction: options?.trx || null,
328
- simplified: false
324
+ transaction: options?.trx || null
329
325
  },
330
326
  createJsonRestContext(options?.context || null)
331
327
  );
@@ -11,7 +11,7 @@ import {
11
11
  createJsonApiInputRecord,
12
12
  createJsonApiRelationship,
13
13
  createJsonRestContext,
14
- simplifyJsonApiDocument
14
+ extractJsonRestCollectionRows
15
15
  } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
16
16
  import { OWNER_ROLE_ID } from "../../../shared/roles.js";
17
17
 
@@ -84,20 +84,27 @@ function createRepository({ api, knex } = {}) {
84
84
 
85
85
  const withTransaction = createWithTransaction(knex);
86
86
 
87
- async function queryMemberships(filters = {}, options = {}, { includeUser = false } = {}) {
88
- const result = await api.resources.workspaceMemberships.query(
89
- {
90
- queryParams: {
91
- filters,
92
- ...(includeUser ? { include: ["user"] } : {})
87
+ async function queryMemberships(filters = {}, options = {}, { include = [] } = {}) {
88
+ const normalizedInclude = Array.from(
89
+ new Set(
90
+ (Array.isArray(include) ? include : [])
91
+ .map((entry) => normalizeText(entry))
92
+ .filter(Boolean)
93
+ )
94
+ );
95
+ return extractJsonRestCollectionRows(
96
+ await api.resources.workspaceMemberships.query(
97
+ {
98
+ queryParams: {
99
+ filters,
100
+ ...(normalizedInclude.length > 0 ? { include: normalizedInclude } : {})
101
+ },
102
+ transaction: options?.trx || null,
103
+ simplified: true
93
104
  },
94
- transaction: options?.trx || null,
95
- simplified: false
96
- },
97
- createJsonRestContext(options?.context || null)
105
+ createJsonRestContext(options?.context || null)
106
+ )
98
107
  );
99
-
100
- return Array.isArray(simplifyJsonApiDocument(result)) ? simplifyJsonApiDocument(result) : [];
101
108
  }
102
109
 
103
110
  async function findByWorkspaceIdAndUserId(workspaceId, userId, options = {}) {
@@ -141,8 +148,7 @@ function createRepository({ api, knex } = {}) {
141
148
  id: existing.id
142
149
  }
143
150
  ),
144
- transaction: options?.trx || null,
145
- simplified: false
151
+ transaction: options?.trx || null
146
152
  },
147
153
  createJsonRestContext(options?.context || null)
148
154
  );
@@ -168,8 +174,7 @@ function createRepository({ api, knex } = {}) {
168
174
  })
169
175
  }
170
176
  ),
171
- transaction: options?.trx || null,
172
- simplified: false
177
+ transaction: options?.trx || null
173
178
  },
174
179
  createJsonRestContext(options?.context || null)
175
180
  );
@@ -216,8 +221,7 @@ function createRepository({ api, knex } = {}) {
216
221
  })
217
222
  }
218
223
  ),
219
- transaction: options?.trx || null,
220
- simplified: false
224
+ transaction: options?.trx || null
221
225
  },
222
226
  createJsonRestContext(options?.context || null)
223
227
  );
@@ -242,8 +246,7 @@ function createRepository({ api, knex } = {}) {
242
246
  id: existing.id
243
247
  }
244
248
  ),
245
- transaction: options?.trx || null,
246
- simplified: false
249
+ transaction: options?.trx || null
247
250
  },
248
251
  createJsonRestContext(options?.context || null)
249
252
  );
@@ -263,7 +266,7 @@ function createRepository({ api, knex } = {}) {
263
266
  status: "active"
264
267
  },
265
268
  options,
266
- { includeUser: true }
269
+ { include: ["user"] }
267
270
  );
268
271
 
269
272
  const members = rows
@@ -9,7 +9,7 @@ import {
9
9
  createJsonApiInputRecord,
10
10
  createJsonApiRelationship,
11
11
  createJsonRestContext,
12
- simplifyJsonApiDocument
12
+ extractJsonRestCollectionRows
13
13
  } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
14
14
 
15
15
  const RESOURCE_TYPE = "workspaces";
@@ -54,19 +54,20 @@ function createRepository({ api, knex } = {}) {
54
54
  const withTransaction = createWithTransaction(knex);
55
55
 
56
56
  async function queryFirst(filters = {}, options = {}) {
57
- const result = await api.resources.workspaces.query(
58
- {
59
- queryParams: {
60
- filters
57
+ const rows = extractJsonRestCollectionRows(
58
+ await api.resources.workspaces.query(
59
+ {
60
+ queryParams: {
61
+ filters
62
+ },
63
+ transaction: options?.trx || null,
64
+ simplified: true
61
65
  },
62
- transaction: options?.trx || null,
63
- simplified: false
64
- },
65
- createJsonRestContext(options?.context || null)
66
+ createJsonRestContext(options?.context || null)
67
+ )
66
68
  );
67
69
 
68
- const rows = simplifyJsonApiDocument(result);
69
- return normalizeWorkspaceRecord(Array.isArray(rows) ? rows[0] || null : null);
70
+ return normalizeWorkspaceRecord(rows[0] || null);
70
71
  }
71
72
 
72
73
  async function findById(workspaceId, options = {}) {
@@ -93,24 +94,24 @@ function createRepository({ api, knex } = {}) {
93
94
  return null;
94
95
  }
95
96
 
96
- const result = await api.resources.workspaces.query(
97
- {
98
- queryParams: {
99
- filters: {
100
- owner: normalizedUserId,
101
- isPersonal: true
102
- }
97
+ const rows = extractJsonRestCollectionRows(
98
+ await api.resources.workspaces.query(
99
+ {
100
+ queryParams: {
101
+ filters: {
102
+ owner: normalizedUserId,
103
+ isPersonal: true
104
+ }
105
+ },
106
+ transaction: options?.trx || null,
107
+ simplified: true
103
108
  },
104
- transaction: options?.trx || null,
105
- simplified: false
106
- },
107
- createJsonRestContext(options?.context || null)
109
+ createJsonRestContext(options?.context || null)
110
+ )
108
111
  );
109
112
 
110
- const rows = Array.isArray(simplifyJsonApiDocument(result))
111
- ? simplifyJsonApiDocument(result).map((row) => normalizeWorkspaceRecord(row)).filter(Boolean)
112
- : [];
113
- rows.sort((left, right) => {
113
+ const normalizedRows = rows.map((row) => normalizeWorkspaceRecord(row)).filter(Boolean);
114
+ normalizedRows.sort((left, right) => {
114
115
  const leftId = Number(left?.id);
115
116
  const rightId = Number(right?.id);
116
117
  if (Number.isFinite(leftId) && Number.isFinite(rightId)) {
@@ -118,7 +119,7 @@ function createRepository({ api, knex } = {}) {
118
119
  }
119
120
  return String(left?.id || "").localeCompare(String(right?.id || ""));
120
121
  });
121
- return rows[0] || null;
122
+ return normalizedRows[0] || null;
122
123
  }
123
124
 
124
125
  async function insert(payload = {}, options = {}) {
@@ -149,13 +150,12 @@ function createRepository({ api, knex } = {}) {
149
150
  relationships: createWorkspaceRelationships({ ownerUserId })
150
151
  }
151
152
  ),
152
- transaction: options?.trx || null,
153
- simplified: false
153
+ transaction: options?.trx || null
154
154
  },
155
155
  createJsonRestContext(options?.context || null)
156
156
  );
157
157
 
158
- return normalizeWorkspaceRecord(simplifyJsonApiDocument(created));
158
+ return normalizeWorkspaceRecord(created);
159
159
  } catch (error) {
160
160
  if (!isDuplicateEntryError(error)) {
161
161
  throw error;
@@ -195,13 +195,12 @@ function createRepository({ api, knex } = {}) {
195
195
  relationships
196
196
  }
197
197
  ),
198
- transaction: options?.trx || null,
199
- simplified: false
198
+ transaction: options?.trx || null
200
199
  },
201
200
  createJsonRestContext(options?.context || null)
202
201
  );
203
202
 
204
- return normalizeWorkspaceRecord(simplifyJsonApiDocument(updated));
203
+ return normalizeWorkspaceRecord(updated);
205
204
  }
206
205
 
207
206
  async function listForUserId(userId, options = {}) {
@@ -210,22 +209,23 @@ function createRepository({ api, knex } = {}) {
210
209
  return [];
211
210
  }
212
211
 
213
- const result = await api.resources.workspaceMemberships.query(
214
- {
215
- queryParams: {
216
- filters: {
217
- user: normalizedUserId,
218
- status: "active"
212
+ const rows = extractJsonRestCollectionRows(
213
+ await api.resources.workspaceMemberships.query(
214
+ {
215
+ queryParams: {
216
+ filters: {
217
+ user: normalizedUserId,
218
+ status: "active"
219
+ },
220
+ include: ["workspace"]
219
221
  },
220
- include: ["workspace"]
222
+ transaction: options?.trx || null,
223
+ simplified: true
221
224
  },
222
- transaction: options?.trx || null,
223
- simplified: false
224
- },
225
- createJsonRestContext(options?.context || null)
225
+ createJsonRestContext(options?.context || null)
226
+ )
226
227
  );
227
228
 
228
- const rows = Array.isArray(simplifyJsonApiDocument(result)) ? simplifyJsonApiDocument(result) : [];
229
229
  const workspaces = rows
230
230
  .map((row) => {
231
231
  const workspace = normalizeWorkspaceRecord(row?.workspace);
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  createJsonApiInputRecord,
9
9
  createJsonRestContext,
10
- simplifyJsonApiDocument
10
+ extractJsonRestCollectionRows
11
11
  } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
12
12
  import { resolveWorkspaceThemePalettes } from "../../shared/settings.js";
13
13
 
@@ -63,19 +63,20 @@ function createRepository({ api, knex } = {}) {
63
63
  const withTransaction = createWithTransaction(knex);
64
64
 
65
65
  async function queryFirst(filters = {}, options = {}) {
66
- const result = await api.resources.workspaceSettings.query(
67
- {
68
- queryParams: {
69
- filters
66
+ const rows = extractJsonRestCollectionRows(
67
+ await api.resources.workspaceSettings.query(
68
+ {
69
+ queryParams: {
70
+ filters
71
+ },
72
+ transaction: options?.trx || null,
73
+ simplified: true
70
74
  },
71
- transaction: options?.trx || null,
72
- simplified: false
73
- },
74
- createJsonRestContext(options?.context || null)
75
+ createJsonRestContext(options?.context || null)
76
+ )
75
77
  );
76
78
 
77
- const rows = simplifyJsonApiDocument(result);
78
- return Array.isArray(rows) ? rows[0] || null : null;
79
+ return rows[0] || null;
79
80
  }
80
81
 
81
82
  async function findByWorkspaceId(workspaceId, options = {}) {
@@ -108,8 +109,7 @@ function createRepository({ api, knex } = {}) {
108
109
  id: normalizedWorkspaceId
109
110
  }
110
111
  ),
111
- transaction: options?.trx || null,
112
- simplified: false
112
+ transaction: options?.trx || null
113
113
  },
114
114
  createJsonRestContext(options?.context || null)
115
115
  );
@@ -148,8 +148,7 @@ function createRepository({ api, knex } = {}) {
148
148
  id: normalizedWorkspaceId
149
149
  }
150
150
  ),
151
- transaction: options?.trx || null,
152
- simplified: false
151
+ transaction: options?.trx || null
153
152
  },
154
153
  createJsonRestContext(options?.context || null)
155
154
  );
@@ -15,34 +15,34 @@ function createKnexStub() {
15
15
  return knex;
16
16
  }
17
17
 
18
- function toWorkspaceInviteResource(row = {}, { includeWorkspace = false } = {}) {
18
+ function asCollectionDocument(rows = []) {
19
+ return {
20
+ data: Array.isArray(rows) ? rows : []
21
+ };
22
+ }
23
+
24
+ function toWorkspaceInviteRow(row = {}) {
19
25
  return {
20
- type: "workspaceInvites",
21
26
  id: String(row.id || ""),
22
- attributes: {
23
- email: row.email,
24
- roleSid: row.roleSid,
25
- status: row.status,
26
- tokenHash: row.tokenHash,
27
- expiresAt: row.expiresAt,
28
- acceptedAt: row.acceptedAt,
29
- revokedAt: row.revokedAt,
30
- createdAt: row.createdAt,
31
- updatedAt: row.updatedAt
27
+ workspaceId: row?.workspace?.id == null ? null : String(row.workspace.id),
28
+ workspace: row?.workspace?.id == null ? null : {
29
+ ...row.workspace,
30
+ id: String(row.workspace.id)
32
31
  },
33
- relationships: {
34
- workspace: {
35
- data: row?.workspace?.id == null
36
- ? null
37
- : {
38
- type: "workspaces",
39
- id: String(row.workspace.id)
40
- }
41
- },
42
- invitedByUser: {
43
- data: row?.invitedByUser?.id == null ? null : { type: "userProfiles", id: String(row.invitedByUser.id) }
44
- }
45
- }
32
+ email: row.email,
33
+ roleSid: row.roleSid,
34
+ status: row.status,
35
+ tokenHash: row.tokenHash,
36
+ invitedByUserId: row?.invitedByUser?.id == null ? null : String(row.invitedByUser.id),
37
+ invitedByUser: row?.invitedByUser?.id == null ? null : {
38
+ ...row.invitedByUser,
39
+ id: String(row.invitedByUser.id)
40
+ },
41
+ expiresAt: row.expiresAt,
42
+ acceptedAt: row.acceptedAt,
43
+ revokedAt: row.revokedAt,
44
+ createdAt: row.createdAt,
45
+ updatedAt: row.updatedAt
46
46
  };
47
47
  }
48
48
 
@@ -60,7 +60,6 @@ function createWorkspaceInvitesApiStub({
60
60
  workspaceInvites: {
61
61
  async query({ queryParams }) {
62
62
  const filters = queryParams?.filters || {};
63
- const includeWorkspace = Array.isArray(queryParams?.include) && queryParams.include.includes("workspace");
64
63
  const matching = rows.filter((row) => {
65
64
  if (Object.hasOwn(filters, "id") && String(row.id) !== String(filters.id)) {
66
65
  return false;
@@ -80,25 +79,9 @@ function createWorkspaceInvitesApiStub({
80
79
  return true;
81
80
  });
82
81
 
83
- return {
84
- data: matching.map((row) => toWorkspaceInviteResource(row, { includeWorkspace })),
85
- included: includeWorkspace
86
- ? matching
87
- .filter((row) => row?.workspace?.id != null)
88
- .map((row) => ({
89
- type: "workspaces",
90
- id: String(row.workspace.id),
91
- attributes: {
92
- slug: row.workspace.slug,
93
- name: row.workspace.name,
94
- avatarUrl: row.workspace.avatarUrl
95
- }
96
- }))
97
- : []
98
- };
82
+ return asCollectionDocument(matching.map((row) => toWorkspaceInviteRow(row)));
99
83
  },
100
84
  async post(payload) {
101
- assert.equal(payload?.simplified, false);
102
85
  const inputRecord = payload?.inputRecord?.data || {};
103
86
  state.postPayload = inputRecord;
104
87
  const row = {
@@ -119,10 +102,9 @@ function createWorkspaceInvitesApiStub({
119
102
  };
120
103
  rows.push(row);
121
104
  rowById.set("1", row);
122
- return { data: toWorkspaceInviteResource(row) };
105
+ return toWorkspaceInviteRow(row);
123
106
  },
124
107
  async patch(payload) {
125
- assert.equal(payload?.simplified, false);
126
108
  const inputRecord = payload?.inputRecord?.data || {};
127
109
  state.patchPayloads.push(inputRecord);
128
110
  const existing = rowById.get(String(inputRecord.id));
@@ -134,7 +116,7 @@ function createWorkspaceInvitesApiStub({
134
116
  rowById.set(String(inputRecord.id), updated);
135
117
  }
136
118
  const updatedRow = rowById.get(String(inputRecord.id)) || null;
137
- return updatedRow ? { data: toWorkspaceInviteResource(updatedRow) } : null;
119
+ return updatedRow ? toWorkspaceInviteRow(updatedRow) : null;
138
120
  }
139
121
  }
140
122
  }
@@ -15,29 +15,31 @@ function createKnexStub() {
15
15
  return knex;
16
16
  }
17
17
 
18
- function toWorkspaceMembershipResource(row = {}) {
18
+ function asCollectionDocument(rows = []) {
19
+ return {
20
+ data: Array.isArray(rows) ? rows : []
21
+ };
22
+ }
23
+
24
+ function toWorkspaceMembershipRow(row = {}) {
19
25
  return {
20
- type: "workspaceMemberships",
21
26
  id: String(row.id || ""),
22
- attributes: {
23
- roleSid: row.roleSid,
24
- status: row.status,
25
- createdAt: row.createdAt,
26
- updatedAt: row.updatedAt
27
+ workspaceId: row?.workspace?.id == null ? null : String(row.workspace.id),
28
+ workspace: row?.workspace?.id == null ? null : {
29
+ ...row.workspace,
30
+ id: String(row.workspace.id)
27
31
  },
28
- relationships: {
29
- workspace: {
30
- data: row?.workspace?.id == null ? null : { type: "workspaces", id: String(row.workspace.id) }
31
- },
32
- user: {
33
- data: row?.user?.id == null
34
- ? null
35
- : {
36
- type: "userProfiles",
37
- id: String(row.user.id)
38
- }
39
- }
40
- }
32
+ userId: row?.user?.id == null ? null : String(row.user.id),
33
+ user: row?.user?.id == null
34
+ ? null
35
+ : {
36
+ ...row.user,
37
+ id: String(row.user.id)
38
+ },
39
+ roleSid: row.roleSid,
40
+ status: row.status,
41
+ createdAt: row.createdAt,
42
+ updatedAt: row.updatedAt
41
43
  };
42
44
  }
43
45
 
@@ -48,35 +50,25 @@ function createWorkspaceMembershipsApiStub({
48
50
  } = {}) {
49
51
  const state = {
50
52
  postPayload: null,
51
- patchPayload: null
53
+ patchPayload: null,
54
+ queryCalls: []
52
55
  };
53
56
 
54
57
  const api = {
55
58
  resources: {
56
59
  workspaceMemberships: {
57
60
  async query({ queryParams }) {
61
+ state.queryCalls.push(queryParams || {});
58
62
  const filters = queryParams?.filters || {};
59
63
  const includeUser = Array.isArray(queryParams?.include) && queryParams.include.includes("user");
60
64
 
61
65
  if (Object.hasOwn(filters, "workspace") && Object.hasOwn(filters, "user")) {
62
66
  const row = rowByComposite.get(`${filters.workspace}:${filters.user}`) || null;
63
- return { data: row ? [toWorkspaceMembershipResource(row)] : [] };
67
+ return asCollectionDocument(row ? [toWorkspaceMembershipRow(row)] : []);
64
68
  }
65
69
 
66
70
  if (Object.hasOwn(filters, "workspace") && Object.hasOwn(filters, "status") && includeUser) {
67
- return {
68
- data: memberSummaryRows.map((row) => toWorkspaceMembershipResource(row)),
69
- included: memberSummaryRows
70
- .filter((row) => row?.user?.id != null)
71
- .map((row) => ({
72
- type: "userProfiles",
73
- id: String(row.user.id),
74
- attributes: {
75
- displayName: row.user.displayName,
76
- email: row.user.email
77
- }
78
- }))
79
- };
71
+ return asCollectionDocument(memberSummaryRows.map((row) => toWorkspaceMembershipRow(row)));
80
72
  }
81
73
 
82
74
  if (Object.hasOwn(filters, "user") && Object.hasOwn(filters, "status")) {
@@ -84,13 +76,12 @@ function createWorkspaceMembershipsApiStub({
84
76
  String(row?.user?.id || "") === String(filters.user) &&
85
77
  String(row?.status || "") === String(filters.status)
86
78
  ));
87
- return { data: rows.map((row) => toWorkspaceMembershipResource(row)) };
79
+ return asCollectionDocument(rows.map((row) => toWorkspaceMembershipRow(row)));
88
80
  }
89
81
 
90
- return { data: [] };
82
+ return asCollectionDocument([]);
91
83
  },
92
84
  async post(payload) {
93
- assert.equal(payload?.simplified, false);
94
85
  const inputRecord = payload?.inputRecord?.data || {};
95
86
  state.postPayload = inputRecord;
96
87
  const row = rowById.get("1") || {
@@ -104,10 +95,9 @@ function createWorkspaceMembershipsApiStub({
104
95
  };
105
96
  rowByComposite.set(`${row.workspace.id}:${row.user.id}`, row);
106
97
  rowById.set(String(row.id), row);
107
- return { data: toWorkspaceMembershipResource(row) };
98
+ return toWorkspaceMembershipRow(row);
108
99
  },
109
100
  async patch(payload) {
110
- assert.equal(payload?.simplified, false);
111
101
  const inputRecord = payload?.inputRecord?.data || {};
112
102
  state.patchPayload = inputRecord;
113
103
  const existing = rowById.get(String(inputRecord.id));
@@ -120,7 +110,7 @@ function createWorkspaceMembershipsApiStub({
120
110
  };
121
111
  rowById.set(String(updated.id), updated);
122
112
  rowByComposite.set(`${updated.workspace.id}:${updated.user.id}`, updated);
123
- return { data: toWorkspaceMembershipResource(updated) };
113
+ return toWorkspaceMembershipRow(updated);
124
114
  }
125
115
  }
126
116
  }
@@ -250,3 +240,23 @@ test("workspaceMembershipsRepository.listActiveByWorkspaceId keeps summary rows
250
240
  }
251
241
  ]);
252
242
  });
243
+
244
+ test("workspaceMembershipsRepository.listActiveWorkspaceIdsByUserId returns normalized workspace ids from the canonical resource", async () => {
245
+ const membershipRow = {
246
+ id: "11",
247
+ workspace: { id: "7" },
248
+ user: { id: "9" },
249
+ roleSid: "owner",
250
+ status: "active",
251
+ createdAt: "2026-03-09 00:26:35.710",
252
+ updatedAt: "2026-03-10 00:26:35.710"
253
+ };
254
+ const { api } = createWorkspaceMembershipsApiStub({
255
+ rowByComposite: new Map([["7:9", membershipRow]])
256
+ });
257
+ const repository = createRepository({ api, knex: createKnexStub() });
258
+
259
+ const workspaceIds = await repository.listActiveWorkspaceIdsByUserId("9");
260
+
261
+ assert.deepEqual(workspaceIds, ["7"]);
262
+ });
@@ -20,6 +20,12 @@ function normalizeWorkspaceColor(value) {
20
20
  return typeof value === "string" ? value.toUpperCase() : value;
21
21
  }
22
22
 
23
+ function asCollectionDocument(rows = []) {
24
+ return {
25
+ data: Array.isArray(rows) ? rows : []
26
+ };
27
+ }
28
+
23
29
  function createWorkspaceSettingsApiStub(rowOverrides = {}) {
24
30
  const DEFAULT_WORKSPACE_THEME = resolveWorkspaceThemePalettes({});
25
31
  const STUB_CREATED_AT = "2026-03-09 00:26:35.710";
@@ -51,31 +57,15 @@ function createWorkspaceSettingsApiStub(rowOverrides = {}) {
51
57
  async query({ queryParams }) {
52
58
  const id = String(queryParams?.filters?.id || "");
53
59
  if (!state.row || (id && String(state.row.id) !== id)) {
54
- return { data: [] };
60
+ return asCollectionDocument([]);
55
61
  }
56
62
 
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
- };
63
+ return asCollectionDocument([{
64
+ ...state.row,
65
+ id: String(state.row.id)
66
+ }]);
76
67
  },
77
68
  async post(payload) {
78
- assert.equal(payload?.simplified, false);
79
69
  const inputRecord = payload?.inputRecord?.data || {};
80
70
  const attributes = inputRecord.attributes || {};
81
71
  state.postPayload = inputRecord;
@@ -94,17 +84,11 @@ function createWorkspaceSettingsApiStub(rowOverrides = {}) {
94
84
  updatedAt: toIsoString("2026-03-10 00:00:00.000")
95
85
  };
96
86
  return {
97
- data: {
98
- type: "workspaceSettings",
99
- id: String(state.row.id),
100
- attributes: {
101
- ...state.row
102
- }
103
- }
87
+ ...state.row,
88
+ id: String(state.row.id)
104
89
  };
105
90
  },
106
91
  async patch(payload) {
107
- assert.equal(payload?.simplified, false);
108
92
  const inputRecord = payload?.inputRecord?.data || {};
109
93
  const attributes = inputRecord.attributes || {};
110
94
  state.patchPayload = inputRecord;
@@ -122,13 +106,8 @@ function createWorkspaceSettingsApiStub(rowOverrides = {}) {
122
106
  id: String(inputRecord.id || state.row?.id || "")
123
107
  };
124
108
  return {
125
- data: {
126
- type: "workspaceSettings",
127
- id: String(state.row.id),
128
- attributes: {
129
- ...state.row
130
- }
131
- }
109
+ ...state.row,
110
+ id: String(state.row.id)
132
111
  };
133
112
  }
134
113
  }
@@ -13,50 +13,35 @@ function createKnexStub() {
13
13
  });
14
14
  }
15
15
 
16
- function toWorkspaceResource(row = {}) {
16
+ function asCollectionDocument(rows = []) {
17
+ return {
18
+ data: Array.isArray(rows) ? rows : []
19
+ };
20
+ }
21
+
22
+ function toWorkspaceRow(row = {}) {
17
23
  return {
18
- type: "workspaces",
19
24
  id: String(row.id || ""),
20
- attributes: {
21
- slug: row.slug,
22
- name: row.name,
23
- isPersonal: row.isPersonal,
24
- avatarUrl: row.avatarUrl,
25
- createdAt: row.createdAt,
26
- updatedAt: row.updatedAt,
27
- deletedAt: row.deletedAt
28
- },
29
- relationships: {
30
- owner: {
31
- data: row.ownerUserId == null
32
- ? null
33
- : {
34
- type: "userProfiles",
35
- id: String(row.ownerUserId)
36
- }
37
- }
38
- }
25
+ slug: row.slug,
26
+ name: row.name,
27
+ ownerUserId: row.ownerUserId == null ? null : String(row.ownerUserId),
28
+ isPersonal: row.isPersonal,
29
+ avatarUrl: row.avatarUrl,
30
+ createdAt: row.createdAt,
31
+ updatedAt: row.updatedAt,
32
+ deletedAt: row.deletedAt
39
33
  };
40
34
  }
41
35
 
42
- function toWorkspaceMembershipResource(row = {}) {
36
+ function toWorkspaceMembershipRow(row = {}) {
43
37
  return {
44
- type: "workspaceMemberships",
45
38
  id: String(row.id || ""),
46
- attributes: {
47
- roleSid: row.roleSid,
48
- status: row.status,
49
- createdAt: row.createdAt,
50
- updatedAt: row.updatedAt
51
- },
52
- relationships: {
53
- user: {
54
- data: row?.user?.id == null ? null : { type: "userProfiles", id: String(row.user.id) }
55
- },
56
- workspace: {
57
- data: row?.workspace?.id == null ? null : { type: "workspaces", id: String(row.workspace.id) }
58
- }
59
- }
39
+ roleSid: row.roleSid,
40
+ status: row.status,
41
+ createdAt: row.createdAt,
42
+ updatedAt: row.updatedAt,
43
+ user: row?.user?.id == null ? null : { ...row.user, id: String(row.user.id) },
44
+ workspace: row?.workspace?.id == null ? null : toWorkspaceRow(row.workspace)
60
45
  };
61
46
  }
62
47
 
@@ -80,23 +65,22 @@ function createWorkspacesApiStub({
80
65
 
81
66
  if (Object.hasOwn(filters, "id")) {
82
67
  const row = rowsById.get(String(filters.id)) || null;
83
- return { data: row ? [toWorkspaceResource(row)] : [] };
68
+ return asCollectionDocument(row ? [toWorkspaceRow(row)] : []);
84
69
  }
85
70
 
86
71
  if (Object.hasOwn(filters, "slug")) {
87
72
  const row = rowsBySlug.get(String(filters.slug)) || null;
88
- return { data: row ? [toWorkspaceResource(row)] : [] };
73
+ return asCollectionDocument(row ? [toWorkspaceRow(row)] : []);
89
74
  }
90
75
 
91
76
  if (Object.hasOwn(filters, "owner") && Object.hasOwn(filters, "isPersonal")) {
92
77
  const rows = personalRowsByOwnerId.get(String(filters.owner)) || [];
93
- return { data: rows.map((row) => toWorkspaceResource(row)) };
78
+ return asCollectionDocument(rows.map((row) => toWorkspaceRow(row)));
94
79
  }
95
80
 
96
- return { data: [] };
81
+ return asCollectionDocument([]);
97
82
  },
98
83
  async post(payload) {
99
- assert.equal(payload?.simplified, false);
100
84
  const inputRecord = payload?.inputRecord?.data || {};
101
85
  state.postPayload = inputRecord;
102
86
  if (insertError) {
@@ -118,10 +102,9 @@ function createWorkspacesApiStub({
118
102
  if (row.slug) {
119
103
  rowsBySlug.set(row.slug, row);
120
104
  }
121
- return { data: toWorkspaceResource(row) };
105
+ return toWorkspaceRow(row);
122
106
  },
123
107
  async patch(payload) {
124
- assert.equal(payload?.simplified, false);
125
108
  const inputRecord = payload?.inputRecord?.data || {};
126
109
  state.patchPayload = inputRecord;
127
110
  const existing = rowsById.get(String(inputRecord.id)) || {
@@ -139,29 +122,21 @@ function createWorkspacesApiStub({
139
122
  if (updated.slug) {
140
123
  rowsBySlug.set(String(updated.slug), updated);
141
124
  }
142
- return { data: toWorkspaceResource(updated) };
125
+ return toWorkspaceRow(updated);
143
126
  }
144
127
  },
145
128
  workspaceMemberships: {
146
129
  async query({ queryParams }) {
147
130
  const filters = queryParams?.filters || {};
148
- const includeWorkspace = Array.isArray(queryParams?.include) && queryParams.include.includes("workspace");
149
131
  if (Object.hasOwn(filters, "user") && Object.hasOwn(filters, "status")) {
150
132
  const rows = membershipRows.filter((row) => (
151
133
  String(row?.user?.id || "") === String(filters.user) &&
152
134
  String(row?.status || "") === String(filters.status)
153
135
  ));
154
- return {
155
- data: rows.map((row) => toWorkspaceMembershipResource(row)),
156
- included: includeWorkspace
157
- ? rows
158
- .filter((row) => row?.workspace?.id != null)
159
- .map((row) => toWorkspaceResource(row.workspace))
160
- : []
161
- };
136
+ return asCollectionDocument(rows.map((row) => toWorkspaceMembershipRow(row)));
162
137
  }
163
138
 
164
- return { data: [] };
139
+ return asCollectionDocument([]);
165
140
  }
166
141
  }
167
142
  }