@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
@@ -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.21",
4
+ version: "0.1.23",
5
5
  kind: "runtime",
6
6
  description: "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
7
7
  dependsOn: [
@@ -112,7 +112,7 @@ export default Object.freeze({
112
112
  mutations: {
113
113
  dependencies: {
114
114
  runtime: {
115
- "@jskit-ai/users-core": "0.1.55"
115
+ "@jskit-ai/users-core": "0.1.57"
116
116
  },
117
117
  dev: {}
118
118
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/workspaces-core",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -18,11 +18,11 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@fastify/type-provider-typebox": "^6.1.0",
21
- "@jskit-ai/auth-core": "0.1.44",
22
- "@jskit-ai/database-runtime": "0.1.45",
23
- "@jskit-ai/http-runtime": "0.1.44",
24
- "@jskit-ai/kernel": "0.1.45",
25
- "@jskit-ai/users-core": "0.1.55",
21
+ "@jskit-ai/auth-core": "0.1.46",
22
+ "@jskit-ai/database-runtime": "0.1.47",
23
+ "@jskit-ai/http-runtime": "0.1.46",
24
+ "@jskit-ai/kernel": "0.1.47",
25
+ "@jskit-ai/users-core": "0.1.57",
26
26
  "typebox": "^1.0.81"
27
27
  }
28
28
  }
@@ -1,38 +1,39 @@
1
- import { resolveInsertedRecordId } from "@jskit-ai/database-runtime/shared";
1
+ import { createCrudResourceRuntime } from "@jskit-ai/crud-core/server/resourceRuntime";
2
2
  import {
3
3
  normalizeLowerText,
4
- normalizeDbRecordId,
5
4
  normalizeRecordId,
6
5
  normalizeText,
7
- toIsoString,
8
- toNullableIso,
9
- toNullableDateTime,
10
6
  nowDb,
11
- isDuplicateEntryError,
12
- createWithTransaction
7
+ isDuplicateEntryError
13
8
  } from "./repositoryUtils.js";
9
+ import { workspaceInvitesResource } from "../resources/workspaceInvitesResource.js";
14
10
 
15
- function mapRow(row) {
16
- if (!row) {
11
+ const REPOSITORY_CONFIG = Object.freeze({
12
+ context: "internal.repository.workspace-invites"
13
+ });
14
+
15
+ function normalizeInviteRecord(payload = {}) {
16
+ if (!payload) {
17
+ return null;
18
+ }
19
+ return workspaceInvitesResource.operations.view.outputValidator.normalize(payload);
20
+ }
21
+
22
+ function normalizeInvitePatchPayload(payload = {}) {
23
+ return workspaceInvitesResource.operations.patch.bodyValidator.normalize(payload);
24
+ }
25
+
26
+ function normalizeInviteWithWorkspace(payload = {}) {
27
+ const invite = normalizeInviteRecord(payload);
28
+ if (!invite) {
17
29
  return null;
18
30
  }
19
31
 
20
32
  return {
21
- id: normalizeDbRecordId(row.id, { fallback: "" }),
22
- workspaceId: normalizeDbRecordId(row.workspace_id, { fallback: "" }),
23
- email: normalizeLowerText(row.email),
24
- roleSid: normalizeLowerText(row.role_sid || "member") || "member",
25
- status: normalizeLowerText(row.status || "pending") || "pending",
26
- tokenHash: normalizeText(row.token_hash),
27
- invitedByUserId: row.invited_by_user_id == null ? null : normalizeDbRecordId(row.invited_by_user_id, { fallback: null }),
28
- expiresAt: toNullableIso(row.expires_at),
29
- acceptedAt: toNullableIso(row.accepted_at),
30
- revokedAt: toNullableIso(row.revoked_at),
31
- createdAt: toIsoString(row.created_at),
32
- updatedAt: toIsoString(row.updated_at),
33
- workspaceSlug: row.workspace_slug ? normalizeText(row.workspace_slug) : undefined,
34
- workspaceName: row.workspace_name ? normalizeText(row.workspace_name) : undefined,
35
- workspaceAvatarUrl: row.workspace_avatar_url ? normalizeText(row.workspace_avatar_url) : undefined
33
+ ...invite,
34
+ workspaceSlug: payload?.workspace_slug ? normalizeText(payload.workspace_slug) : undefined,
35
+ workspaceName: payload?.workspace_name ? normalizeText(payload.workspace_name) : undefined,
36
+ workspaceAvatarUrl: payload?.workspace_avatar_url ? normalizeText(payload.workspace_avatar_url) : undefined
36
37
  };
37
38
  }
38
39
 
@@ -47,14 +48,15 @@ function createRepository(knex) {
47
48
  if (typeof knex !== "function") {
48
49
  throw new TypeError("workspaceInvitesRepository requires knex.");
49
50
  }
50
- const withTransaction = createWithTransaction(knex);
51
+ const resourceRuntime = createCrudResourceRuntime(workspaceInvitesResource, knex, REPOSITORY_CONFIG);
52
+ const withTransaction = resourceRuntime.withTransaction;
51
53
 
52
54
  async function findPendingByTokenHash(tokenHash, options = {}) {
53
55
  const client = options?.trx || knex;
54
56
  const row = await client("workspace_invites")
55
57
  .where({ token_hash: normalizeText(tokenHash), status: "pending" })
56
58
  .first();
57
- return mapRow(row);
59
+ return normalizeInviteRecord(row);
58
60
  }
59
61
 
60
62
  async function listPendingByEmail(email, options = {}) {
@@ -70,7 +72,7 @@ function createRepository(knex) {
70
72
  .orderBy("wi.created_at", "desc")
71
73
  .select(WORKSPACE_INVITE_WITH_WORKSPACE_SELECT);
72
74
 
73
- return rows.map(mapRow).filter(Boolean);
75
+ return rows.map(normalizeInviteWithWorkspace).filter(Boolean);
74
76
  }
75
77
 
76
78
  async function listPendingByWorkspaceIdWithWorkspace(workspaceId, options = {}) {
@@ -86,38 +88,32 @@ function createRepository(knex) {
86
88
  .orderBy("wi.created_at", "desc")
87
89
  .select(WORKSPACE_INVITE_WITH_WORKSPACE_SELECT);
88
90
 
89
- return rows.map(mapRow).filter(Boolean);
91
+ return rows.map(normalizeInviteWithWorkspace).filter(Boolean);
90
92
  }
91
93
 
92
94
  async function insert(payload = {}, options = {}) {
93
95
  const client = options?.trx || knex;
94
- const source = payload && typeof payload === "object" ? payload : {};
96
+ const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
95
97
  const workspaceId = normalizeRecordId(source.workspaceId, { fallback: null });
96
98
  if (!workspaceId) {
97
99
  throw new TypeError("workspaceInvitesRepository.insert requires workspaceId.");
98
100
  }
99
101
 
100
- const insertPayload = {
101
- workspace_id: workspaceId,
102
- email: normalizeLowerText(source.email),
103
- role_sid: normalizeLowerText(source.roleSid || "member") || "member",
102
+ const createPayload = {
103
+ ...source,
104
+ workspaceId,
105
+ roleSid: normalizeLowerText(source.roleSid || "member") || "member",
104
106
  status: normalizeLowerText(source.status || "pending") || "pending",
105
- token_hash: normalizeText(source.tokenHash),
106
- invited_by_user_id: source.invitedByUserId == null ? null : normalizeRecordId(source.invitedByUserId, { fallback: null }),
107
- expires_at: toNullableDateTime(source.expiresAt),
108
- accepted_at: null,
109
- revoked_at: null,
110
- created_at: nowDb(),
111
- updated_at: nowDb()
107
+ acceptedAt: null,
108
+ revokedAt: null
112
109
  };
113
110
 
114
111
  try {
115
- const result = await client("workspace_invites").insert(insertPayload);
116
- const insertedId = resolveInsertedRecordId(result, { fallback: null });
117
- if (insertedId) {
118
- const row = await client("workspace_invites").where({ id: insertedId }).first();
119
- return mapRow(row);
120
- }
112
+ return await resourceRuntime.create(createPayload, {
113
+ ...options,
114
+ trx: client,
115
+ include: "none"
116
+ });
121
117
  } catch (error) {
122
118
  if (!isDuplicateEntryError(error)) {
123
119
  throw error;
@@ -125,10 +121,10 @@ function createRepository(knex) {
125
121
  }
126
122
 
127
123
  const row = await client("workspace_invites")
128
- .where({ workspace_id: insertPayload.workspace_id, email: insertPayload.email, status: "pending" })
124
+ .where({ workspace_id: createPayload.workspaceId, email: createPayload.email, status: "pending" })
129
125
  .orderBy("id", "desc")
130
126
  .first();
131
- return mapRow(row);
127
+ return normalizeInviteRecord(row);
132
128
  }
133
129
 
134
130
  async function expirePendingByWorkspaceIdAndEmail(workspaceId, email, options = {}) {
@@ -138,10 +134,11 @@ function createRepository(knex) {
138
134
  }
139
135
 
140
136
  const client = options?.trx || knex;
137
+ const patch = normalizeInvitePatchPayload({ status: "expired" });
141
138
  await client("workspace_invites")
142
139
  .where({ workspace_id: normalizedWorkspaceId, email: normalizeLowerText(email), status: "pending" })
143
140
  .update({
144
- status: "expired",
141
+ status: patch.status,
145
142
  updated_at: nowDb()
146
143
  });
147
144
  }
@@ -152,14 +149,17 @@ function createRepository(knex) {
152
149
  return;
153
150
  }
154
151
 
155
- const client = options?.trx || knex;
156
- await client("workspace_invites")
157
- .where({ id: normalizedInviteId })
158
- .update({
152
+ await resourceRuntime.updateById(
153
+ normalizedInviteId,
154
+ {
159
155
  status: "accepted",
160
- accepted_at: nowDb(),
161
- updated_at: nowDb()
162
- });
156
+ acceptedAt: new Date()
157
+ },
158
+ {
159
+ ...options,
160
+ include: "none"
161
+ }
162
+ );
163
163
  }
164
164
 
165
165
  async function revokeById(inviteId, options = {}) {
@@ -168,14 +168,17 @@ function createRepository(knex) {
168
168
  return;
169
169
  }
170
170
 
171
- const client = options?.trx || knex;
172
- await client("workspace_invites")
173
- .where({ id: normalizedInviteId })
174
- .update({
171
+ await resourceRuntime.updateById(
172
+ normalizedInviteId,
173
+ {
175
174
  status: "revoked",
176
- revoked_at: nowDb(),
177
- updated_at: nowDb()
178
- });
175
+ revokedAt: new Date()
176
+ },
177
+ {
178
+ ...options,
179
+ include: "none"
180
+ }
181
+ );
179
182
  }
180
183
 
181
184
  async function findPendingByIdForWorkspace(inviteId, workspaceId, options = {}) {
@@ -189,7 +192,7 @@ function createRepository(knex) {
189
192
  const row = await client("workspace_invites")
190
193
  .where({ id: normalizedInviteId, workspace_id: normalizedWorkspaceId, status: "pending" })
191
194
  .first();
192
- return mapRow(row);
195
+ return normalizeInviteRecord(row);
193
196
  }
194
197
 
195
198
  return Object.freeze({
@@ -205,4 +208,4 @@ function createRepository(knex) {
205
208
  });
206
209
  }
207
210
 
208
- export { createRepository, mapRow };
211
+ export { createRepository, normalizeInviteRecord, normalizeInviteWithWorkspace };
@@ -1,32 +1,30 @@
1
+ import { createCrudResourceRuntime } from "@jskit-ai/crud-core/server/resourceRuntime";
1
2
  import {
2
3
  normalizeLowerText,
3
4
  normalizeRecordId,
4
5
  normalizeDbRecordId,
5
6
  normalizeText,
6
- toIsoString,
7
- nowDb,
8
- isDuplicateEntryError,
9
- createWithTransaction
7
+ isDuplicateEntryError
10
8
  } from "./repositoryUtils.js";
11
9
  import { OWNER_ROLE_ID } from "../../../shared/roles.js";
10
+ import { workspaceMembershipsResource } from "../resources/workspaceMembershipsResource.js";
12
11
 
13
- function mapRow(row) {
14
- if (!row) {
12
+ const REPOSITORY_CONFIG = Object.freeze({
13
+ context: "internal.repository.workspace-memberships"
14
+ });
15
+
16
+ function normalizeMembershipRecord(payload = {}) {
17
+ if (!payload) {
15
18
  return null;
16
19
  }
20
+ return workspaceMembershipsResource.operations.view.outputValidator.normalize(payload);
21
+ }
17
22
 
18
- return {
19
- id: normalizeDbRecordId(row.id, { fallback: "" }),
20
- workspaceId: normalizeDbRecordId(row.workspace_id, { fallback: "" }),
21
- userId: normalizeDbRecordId(row.user_id, { fallback: "" }),
22
- roleSid: normalizeLowerText(row.role_sid || "member") || "member",
23
- status: normalizeLowerText(row.status || "active") || "active",
24
- createdAt: toIsoString(row.created_at),
25
- updatedAt: toIsoString(row.updated_at)
26
- };
23
+ function normalizeMembershipPatchPayload(payload = {}) {
24
+ return workspaceMembershipsResource.operations.patch.bodyValidator.normalize(payload);
27
25
  }
28
26
 
29
- function mapMemberSummaryRow(row) {
27
+ function normalizeMemberSummaryRow(row) {
30
28
  if (!row) {
31
29
  return null;
32
30
  }
@@ -44,7 +42,8 @@ function createRepository(knex) {
44
42
  if (typeof knex !== "function") {
45
43
  throw new TypeError("workspaceMembershipsRepository requires knex.");
46
44
  }
47
- const withTransaction = createWithTransaction(knex);
45
+ const resourceRuntime = createCrudResourceRuntime(workspaceMembershipsResource, knex, REPOSITORY_CONFIG);
46
+ const withTransaction = resourceRuntime.withTransaction;
48
47
 
49
48
  async function findByWorkspaceIdAndUserId(workspaceId, userId, options = {}) {
50
49
  const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
@@ -57,7 +56,7 @@ function createRepository(knex) {
57
56
  const row = await client("workspace_memberships")
58
57
  .where({ workspace_id: normalizedWorkspaceId, user_id: normalizedUserId })
59
58
  .first();
60
- return mapRow(row);
59
+ return normalizeMembershipRecord(row);
61
60
  }
62
61
 
63
62
  async function ensureOwnerMembership(workspaceId, userId, options = {}) {
@@ -71,26 +70,37 @@ function createRepository(knex) {
71
70
  const existing = await findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, { trx: client });
72
71
  if (existing) {
73
72
  if (existing.roleSid !== OWNER_ROLE_ID || existing.status !== "active") {
74
- await client("workspace_memberships")
75
- .where({ workspace_id: normalizedWorkspaceId, user_id: normalizedUserId })
76
- .update({
77
- role_sid: OWNER_ROLE_ID,
78
- status: "active",
79
- updated_at: nowDb()
80
- });
73
+ await resourceRuntime.updateById(
74
+ existing.id,
75
+ {
76
+ roleSid: OWNER_ROLE_ID,
77
+ status: "active"
78
+ },
79
+ {
80
+ ...options,
81
+ trx: client,
82
+ include: "none",
83
+ existingRecord: existing
84
+ }
85
+ );
81
86
  }
82
87
  return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, { trx: client });
83
88
  }
84
89
 
85
90
  try {
86
- await client("workspace_memberships").insert({
87
- workspace_id: normalizedWorkspaceId,
88
- user_id: normalizedUserId,
89
- role_sid: OWNER_ROLE_ID,
90
- status: "active",
91
- created_at: nowDb(),
92
- updated_at: nowDb()
93
- });
91
+ await resourceRuntime.create(
92
+ {
93
+ workspaceId: normalizedWorkspaceId,
94
+ userId: normalizedUserId,
95
+ roleSid: OWNER_ROLE_ID,
96
+ status: "active"
97
+ },
98
+ {
99
+ ...options,
100
+ trx: client,
101
+ include: "none"
102
+ }
103
+ );
94
104
  } catch (error) {
95
105
  if (!isDuplicateEntryError(error)) {
96
106
  throw error;
@@ -109,28 +119,49 @@ function createRepository(knex) {
109
119
 
110
120
  const client = options?.trx || knex;
111
121
  const existing = await findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, { trx: client });
112
- const roleSid = normalizeLowerText(patch.roleSid || existing?.roleSid || "member") || "member";
113
- const status = normalizeLowerText(patch.status || existing?.status || "active") || "active";
122
+ const normalizedPatch = normalizeMembershipPatchPayload({
123
+ roleSid: patch?.roleSid ?? existing?.roleSid ?? "member",
124
+ status: patch?.status ?? existing?.status ?? "active"
125
+ });
126
+ const roleSid = normalizeLowerText(normalizedPatch.roleSid || "member") || "member";
127
+ const status = normalizeLowerText(normalizedPatch.status || "active") || "active";
114
128
 
115
129
  if (!existing) {
116
- await client("workspace_memberships").insert({
117
- workspace_id: normalizedWorkspaceId,
118
- user_id: normalizedUserId,
119
- role_sid: roleSid,
120
- status,
121
- created_at: nowDb(),
122
- updated_at: nowDb()
123
- });
130
+ try {
131
+ await resourceRuntime.create(
132
+ {
133
+ workspaceId: normalizedWorkspaceId,
134
+ userId: normalizedUserId,
135
+ roleSid,
136
+ status
137
+ },
138
+ {
139
+ ...options,
140
+ trx: client,
141
+ include: "none"
142
+ }
143
+ );
144
+ } catch (error) {
145
+ if (!isDuplicateEntryError(error)) {
146
+ throw error;
147
+ }
148
+ }
124
149
  return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, { trx: client });
125
150
  }
126
151
 
127
- await client("workspace_memberships")
128
- .where({ workspace_id: normalizedWorkspaceId, user_id: normalizedUserId })
129
- .update({
130
- role_sid: roleSid,
131
- status,
132
- updated_at: nowDb()
133
- });
152
+ await resourceRuntime.updateById(
153
+ existing.id,
154
+ {
155
+ roleSid,
156
+ status
157
+ },
158
+ {
159
+ ...options,
160
+ trx: client,
161
+ include: "none",
162
+ existingRecord: existing
163
+ }
164
+ );
134
165
 
135
166
  return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, { trx: client });
136
167
  }
@@ -154,7 +185,7 @@ function createRepository(knex) {
154
185
  "up.email"
155
186
  ]);
156
187
 
157
- return rows.map(mapMemberSummaryRow).filter(Boolean);
188
+ return rows.map(normalizeMemberSummaryRow).filter(Boolean);
158
189
  }
159
190
 
160
191
  async function listActiveWorkspaceIdsByUserId(userId, options = {}) {
@@ -187,4 +218,4 @@ function createRepository(knex) {
187
218
  });
188
219
  }
189
220
 
190
- export { createRepository, mapRow, mapMemberSummaryRow };
221
+ export { createRepository, normalizeMembershipRecord, normalizeMemberSummaryRow };
@@ -1,41 +1,34 @@
1
- import { resolveInsertedRecordId } from "@jskit-ai/database-runtime/shared";
1
+ import { createCrudResourceRuntime } from "@jskit-ai/crud-core/server/resourceRuntime";
2
2
  import {
3
- normalizeDbRecordId,
4
3
  normalizeRecordId,
5
4
  normalizeText,
6
5
  normalizeLowerText,
7
- toIsoString,
8
- toNullableIso,
9
- nowDb,
10
- isDuplicateEntryError,
11
- createWithTransaction
6
+ isDuplicateEntryError
12
7
  } from "./repositoryUtils.js";
8
+ import { workspacesResource } from "../resources/workspacesResource.js";
13
9
 
14
- function mapRow(row) {
15
- if (!row) {
10
+ const REPOSITORY_CONFIG = Object.freeze({
11
+ context: "internal.repository.workspaces"
12
+ });
13
+
14
+ function normalizeWorkspaceRecord(payload = {}) {
15
+ if (!payload) {
16
16
  return null;
17
17
  }
18
+ return workspacesResource.operations.view.outputValidator.normalize(payload);
19
+ }
18
20
 
19
- return {
20
- id: normalizeDbRecordId(row.id, { fallback: "" }),
21
- slug: normalizeText(row.slug),
22
- name: normalizeText(row.name),
23
- ownerUserId: normalizeDbRecordId(row.owner_user_id, { fallback: "" }),
24
- isPersonal: Boolean(row.is_personal),
25
- avatarUrl: row.avatar_url ? normalizeText(row.avatar_url) : "",
26
- createdAt: toIsoString(row.created_at),
27
- updatedAt: toIsoString(row.updated_at),
28
- deletedAt: toNullableIso(row.deleted_at)
29
- };
21
+ function normalizeCreatePayload(payload = {}) {
22
+ return workspacesResource.operations.create.bodyValidator.normalize(payload);
30
23
  }
31
24
 
32
- function mapMembershipWorkspaceRow(row) {
25
+ function normalizeMembershipWorkspaceRow(row) {
33
26
  if (!row) {
34
27
  return null;
35
28
  }
36
29
 
37
30
  return {
38
- ...mapRow(row),
31
+ ...normalizeWorkspaceRecord(row),
39
32
  roleSid: normalizeLowerText(row.role_sid || "member"),
40
33
  membershipStatus: normalizeLowerText(row.membership_status || "active") || "active"
41
34
  };
@@ -45,7 +38,8 @@ function createRepository(knex) {
45
38
  if (typeof knex !== "function") {
46
39
  throw new TypeError("workspacesRepository requires knex.");
47
40
  }
48
- const withTransaction = createWithTransaction(knex);
41
+ const resourceRuntime = createCrudResourceRuntime(workspacesResource, knex, REPOSITORY_CONFIG);
42
+ const withTransaction = resourceRuntime.withTransaction;
49
43
 
50
44
  function workspaceSelectColumns({ includeMembership = false } = {}) {
51
45
  const columns = [
@@ -71,12 +65,10 @@ function createRepository(knex) {
71
65
  return null;
72
66
  }
73
67
 
74
- const client = options?.trx || knex;
75
- const row = await client("workspaces as w")
76
- .where({ "w.id": normalizedWorkspaceId })
77
- .select(workspaceSelectColumns())
78
- .first();
79
- return mapRow(row);
68
+ return resourceRuntime.findById(normalizedWorkspaceId, {
69
+ ...options,
70
+ include: "none"
71
+ });
80
72
  }
81
73
 
82
74
  async function findBySlug(slug, options = {}) {
@@ -90,7 +82,7 @@ function createRepository(knex) {
90
82
  .where({ "w.slug": normalizedSlug })
91
83
  .select(workspaceSelectColumns())
92
84
  .first();
93
- return mapRow(row);
85
+ return normalizeWorkspaceRecord(row);
94
86
  }
95
87
 
96
88
  async function findPersonalByOwnerUserId(userId, options = {}) {
@@ -105,41 +97,35 @@ function createRepository(knex) {
105
97
  .orderBy("w.id", "asc")
106
98
  .select(workspaceSelectColumns())
107
99
  .first();
108
- return mapRow(row);
100
+ return normalizeWorkspaceRecord(row);
109
101
  }
110
102
 
111
103
  async function insert(payload = {}, options = {}) {
112
104
  const client = options?.trx || knex;
113
- const source = payload && typeof payload === "object" ? payload : {};
114
- const ownerUserId = normalizeRecordId(source.ownerUserId, { fallback: null });
105
+ const normalizedPayload = normalizeCreatePayload(payload);
106
+ const ownerUserId = normalizeRecordId(normalizedPayload.ownerUserId, { fallback: null });
115
107
  if (!ownerUserId) {
116
108
  throw new TypeError("workspacesRepository.insert requires ownerUserId.");
117
109
  }
118
110
 
119
- const insertPayload = {
120
- slug: normalizeLowerText(source.slug),
121
- name: normalizeText(source.name),
122
- owner_user_id: ownerUserId,
123
- is_personal: source.isPersonal ? 1 : 0,
124
- avatar_url: normalizeText(source.avatarUrl),
125
- created_at: nowDb(),
126
- updated_at: nowDb(),
127
- deleted_at: null
111
+ const createPayload = {
112
+ ...normalizedPayload,
113
+ ownerUserId,
114
+ isPersonal: normalizedPayload.isPersonal === true,
115
+ avatarUrl: normalizeText(normalizedPayload.avatarUrl)
128
116
  };
129
117
 
130
118
  try {
131
- const result = await client("workspaces").insert(insertPayload);
132
- const insertedId = resolveInsertedRecordId(result, { fallback: null });
133
- if (insertedId) {
134
- return findById(insertedId, { trx: client });
135
- }
136
- const bySlug = await findBySlug(insertPayload.slug, { trx: client });
137
- return bySlug;
119
+ return await resourceRuntime.create(createPayload, {
120
+ ...options,
121
+ trx: client,
122
+ include: "none"
123
+ });
138
124
  } catch (error) {
139
125
  if (!isDuplicateEntryError(error)) {
140
126
  throw error;
141
127
  }
142
- const bySlug = await findBySlug(insertPayload.slug, { trx: client });
128
+ const bySlug = await findBySlug(createPayload.slug, { trx: client });
143
129
  if (bySlug) {
144
130
  return bySlug;
145
131
  }
@@ -153,21 +139,10 @@ function createRepository(knex) {
153
139
  return null;
154
140
  }
155
141
 
156
- const client = options?.trx || knex;
157
- const source = patch && typeof patch === "object" ? patch : {};
158
- const dbPatch = {
159
- updated_at: nowDb()
160
- };
161
-
162
- if (Object.hasOwn(source, "name")) {
163
- dbPatch.name = normalizeText(source.name);
164
- }
165
- if (Object.hasOwn(source, "avatarUrl")) {
166
- dbPatch.avatar_url = normalizeText(source.avatarUrl);
167
- }
168
-
169
- await client("workspaces").where({ id: normalizedWorkspaceId }).update(dbPatch);
170
- return findById(normalizedWorkspaceId, { trx: client });
142
+ return resourceRuntime.updateById(normalizedWorkspaceId, patch, {
143
+ ...options,
144
+ include: "none"
145
+ });
171
146
  }
172
147
 
173
148
  async function listForUserId(userId, options = {}) {
@@ -185,7 +160,7 @@ function createRepository(knex) {
185
160
  .orderBy("w.id", "asc")
186
161
  .select(workspaceSelectColumns({ includeMembership: true }));
187
162
 
188
- return rows.map(mapMembershipWorkspaceRow).filter(Boolean);
163
+ return rows.map(normalizeMembershipWorkspaceRow).filter(Boolean);
189
164
  }
190
165
 
191
166
  return Object.freeze({
@@ -199,4 +174,4 @@ function createRepository(knex) {
199
174
  });
200
175
  }
201
176
 
202
- export { createRepository, mapRow, mapMembershipWorkspaceRow };
177
+ export { createRepository, normalizeWorkspaceRecord, normalizeMembershipWorkspaceRow };