@jskit-ai/workspaces-core 0.1.91 → 0.1.92

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.91",
4
+ version: "0.1.92",
5
5
  kind: "runtime",
6
6
  description: "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
7
7
  dependsOn: [
@@ -147,10 +147,10 @@ export default Object.freeze({
147
147
  mutations: {
148
148
  dependencies: {
149
149
  runtime: {
150
- "@jskit-ai/json-rest-api-core": "0.1.60",
151
- "@jskit-ai/resource-core": "0.1.60",
152
- "@jskit-ai/resource-crud-core": "0.1.60",
153
- "@jskit-ai/users-core": "0.1.125"
150
+ "@jskit-ai/json-rest-api-core": "0.1.61",
151
+ "@jskit-ai/resource-core": "0.1.61",
152
+ "@jskit-ai/resource-crud-core": "0.1.61",
153
+ "@jskit-ai/users-core": "0.1.126"
154
154
  },
155
155
  dev: {}
156
156
  },
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@jskit-ai/workspaces-core",
3
- "version": "0.1.91",
3
+ "version": "0.1.92",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "exports": {
9
9
  "./server/WorkspacesCoreServiceProvider": "./src/server/WorkspacesCoreServiceProvider.js",
10
- "./server/previewWorkspaceProvisioning": "./src/server/previewWorkspaceProvisioning.js",
11
10
  "./server/validators/routeParamsValidator": "./src/server/common/validators/routeParamsValidator.js",
12
11
  "./server/support/workspaceRouteInput": "./src/server/support/workspaceRouteInput.js",
13
12
  "./shared/jsonApiTransports": "./src/shared/jsonApiTransports.js",
@@ -18,14 +17,14 @@
18
17
  "./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js"
19
18
  },
20
19
  "dependencies": {
21
- "@jskit-ai/auth-core": "0.1.114",
22
- "@jskit-ai/database-runtime": "0.1.115",
23
- "@jskit-ai/http-runtime": "0.1.115",
24
- "@jskit-ai/json-rest-api-core": "0.1.60",
25
- "@jskit-ai/kernel": "0.1.116",
26
- "@jskit-ai/resource-crud-core": "0.1.60",
27
- "@jskit-ai/resource-core": "0.1.60",
28
- "@jskit-ai/users-core": "0.1.125",
20
+ "@jskit-ai/auth-core": "0.1.115",
21
+ "@jskit-ai/database-runtime": "0.1.116",
22
+ "@jskit-ai/http-runtime": "0.1.116",
23
+ "@jskit-ai/json-rest-api-core": "0.1.61",
24
+ "@jskit-ai/kernel": "0.1.117",
25
+ "@jskit-ai/resource-crud-core": "0.1.61",
26
+ "@jskit-ai/resource-core": "0.1.61",
27
+ "@jskit-ai/users-core": "0.1.126",
29
28
  "json-rest-schema": "1.x.x"
30
29
  }
31
30
  }
@@ -15,7 +15,6 @@ test("workspaces-core exports are explicit and aligned with production usage", (
15
15
  packageId: "@jskit-ai/workspaces-core",
16
16
  requiredExports: [
17
17
  "./server/WorkspacesCoreServiceProvider",
18
- "./server/previewWorkspaceProvisioning",
19
18
  "./server/validators/routeParamsValidator",
20
19
  "./server/support/workspaceRouteInput",
21
20
  "./shared/resources/workspaceResource",
@@ -1,227 +0,0 @@
1
- import { normalizeTenancyMode, TENANCY_MODE_NONE, TENANCY_MODE_PERSONAL } from "../shared/tenancyMode.js";
2
- import { resolveTenancyProfile } from "../shared/tenancyProfile.js";
3
- import { OWNER_ROLE_ID } from "../shared/roles.js";
4
- import { resolveWorkspaceThemePalettes } from "../shared/settings.js";
5
-
6
- const WORKSPACE_SLUG_MAX_LENGTH = 48;
7
-
8
- function normalizeText(value = "") {
9
- return String(value || "").trim();
10
- }
11
-
12
- function normalizeLowerText(value = "") {
13
- return normalizeText(value).toLowerCase();
14
- }
15
-
16
- function workspaceSlugPart(value = "") {
17
- const normalized = normalizeLowerText(value)
18
- .replace(/[^a-z0-9]+/g, "-")
19
- .replace(/^-+|-+$/g, "")
20
- .slice(0, WORKSPACE_SLUG_MAX_LENGTH);
21
- return normalized || "workspace";
22
- }
23
-
24
- function usernameBaseFromEmail(email = "") {
25
- const normalizedEmail = normalizeLowerText(email);
26
- const localPart = normalizedEmail.includes("@") ? normalizedEmail.split("@")[0] : normalizedEmail;
27
- return workspaceSlugPart(localPart) || "user";
28
- }
29
-
30
- function buildWorkspaceBaseSlug(profile = {}) {
31
- return workspaceSlugPart(profile.username || profile.displayName || usernameBaseFromEmail(profile.email));
32
- }
33
-
34
- function buildWorkspaceName(profile = {}) {
35
- const displayName = normalizeText(profile.displayName);
36
- if (displayName) {
37
- return `${displayName}'s Workspace`;
38
- }
39
- const email = normalizeLowerText(profile.email);
40
- return email ? `${email}'s Workspace` : "Workspace";
41
- }
42
-
43
- function buildWorkspaceSlugCandidate(baseSlug = "", suffix = 0) {
44
- const base = workspaceSlugPart(baseSlug);
45
- if (suffix < 1) {
46
- return base;
47
- }
48
- const suffixText = `-${suffix + 1}`;
49
- return `${base.slice(0, WORKSPACE_SLUG_MAX_LENGTH - suffixText.length)}${suffixText}`;
50
- }
51
-
52
- function isDuplicateError(error) {
53
- return (
54
- ["23505", "ER_DUP_ENTRY", "SQLITE_CONSTRAINT", "SQLITE_CONSTRAINT_UNIQUE"].includes(String(error?.code || "")) ||
55
- Number(error?.errno) === 1062
56
- );
57
- }
58
-
59
- function normalizeWorkspaceResult(workspace = null) {
60
- if (!workspace) {
61
- return null;
62
- }
63
- return {
64
- id: normalizeText(workspace.id),
65
- slug: normalizeLowerText(workspace.slug)
66
- };
67
- }
68
-
69
- async function resolveUniqueWorkspaceSlug(db, baseSlug = "", { excludeWorkspaceId = "" } = {}) {
70
- const normalizedExcludeWorkspaceId = normalizeText(excludeWorkspaceId);
71
- for (let suffix = 0; suffix < 1000; suffix += 1) {
72
- const candidate = buildWorkspaceSlugCandidate(baseSlug, suffix);
73
- const existing = await db("workspaces")
74
- .where({ slug: candidate })
75
- .first();
76
- if (!existing || normalizeText(existing.id) === normalizedExcludeWorkspaceId) {
77
- return candidate;
78
- }
79
- }
80
- throw new Error("Unable to generate unique preview workspace slug.");
81
- }
82
-
83
- async function hasWorkspaceTables(db) {
84
- return (
85
- (await db.schema.hasTable("workspaces")) &&
86
- (await db.schema.hasTable("workspace_memberships")) &&
87
- (await db.schema.hasTable("workspace_settings"))
88
- );
89
- }
90
-
91
- async function findPreviewWorkspace(db, user = {}, { isPersonal = true } = {}) {
92
- return db("workspaces")
93
- .where({
94
- owner_user_id: user.id,
95
- is_personal: isPersonal
96
- })
97
- .orderBy("id", "asc")
98
- .first();
99
- }
100
-
101
- async function ensureWorkspaceSettings(db, workspace = {}) {
102
- const existing = await db("workspace_settings")
103
- .where({ workspace_id: workspace.id })
104
- .first();
105
- if (existing) {
106
- return false;
107
- }
108
-
109
- const palettes = resolveWorkspaceThemePalettes({});
110
- try {
111
- await db("workspace_settings").insert({
112
- workspace_id: workspace.id,
113
- light_primary_color: palettes.light.color,
114
- light_secondary_color: palettes.light.secondaryColor,
115
- light_surface_color: palettes.light.surfaceColor,
116
- light_surface_variant_color: palettes.light.surfaceVariantColor,
117
- dark_primary_color: palettes.dark.color,
118
- dark_secondary_color: palettes.dark.secondaryColor,
119
- dark_surface_color: palettes.dark.surfaceColor,
120
- dark_surface_variant_color: palettes.dark.surfaceVariantColor,
121
- invites_enabled: true
122
- });
123
- } catch (error) {
124
- if (!isDuplicateError(error)) {
125
- throw error;
126
- }
127
- }
128
- return true;
129
- }
130
-
131
- async function ensureOwnerMembership(db, workspace = {}, user = {}) {
132
- const existing = await db("workspace_memberships")
133
- .where({
134
- workspace_id: workspace.id,
135
- user_id: user.id
136
- })
137
- .first();
138
- if (existing) {
139
- if (existing.role_sid !== OWNER_ROLE_ID || existing.status !== "active") {
140
- await db("workspace_memberships")
141
- .where({ id: existing.id })
142
- .update({
143
- role_sid: OWNER_ROLE_ID,
144
- status: "active",
145
- updated_at: new Date()
146
- });
147
- return true;
148
- }
149
- return false;
150
- }
151
-
152
- try {
153
- await db("workspace_memberships").insert({
154
- workspace_id: workspace.id,
155
- user_id: user.id,
156
- role_sid: OWNER_ROLE_ID,
157
- status: "active"
158
- });
159
- } catch (error) {
160
- if (!isDuplicateError(error)) {
161
- throw error;
162
- }
163
- }
164
- return true;
165
- }
166
-
167
- async function ensurePreviewWorkspace(db, user = {}, profile = {}, { appConfig = {}, tenancyMode = "" } = {}) {
168
- if (!db || typeof db !== "function" || !db.schema || typeof db.schema.hasTable !== "function") {
169
- throw new TypeError("ensurePreviewWorkspace requires a Knex database instance.");
170
- }
171
-
172
- const resolvedTenancyMode = normalizeTenancyMode(tenancyMode || resolveTenancyProfile(appConfig).mode);
173
- if (resolvedTenancyMode === TENANCY_MODE_NONE) {
174
- return {
175
- workspace: null,
176
- skipped: "workspace tenancy is disabled"
177
- };
178
- }
179
-
180
- if (!(await hasWorkspaceTables(db))) {
181
- return {
182
- workspace: null,
183
- skipped: "workspace tables were not found"
184
- };
185
- }
186
-
187
- const isPersonal = resolvedTenancyMode === TENANCY_MODE_PERSONAL;
188
- let workspace = await findPreviewWorkspace(db, user, { isPersonal });
189
- if (!workspace) {
190
- const slug = await resolveUniqueWorkspaceSlug(db, buildWorkspaceBaseSlug(profile));
191
- try {
192
- await db("workspaces").insert({
193
- avatar_url: "",
194
- is_personal: isPersonal,
195
- name: buildWorkspaceName(profile),
196
- owner_user_id: user.id,
197
- slug
198
- });
199
- } catch (error) {
200
- if (!isDuplicateError(error)) {
201
- throw error;
202
- }
203
- }
204
- workspace = await db("workspaces")
205
- .where({ slug })
206
- .first();
207
- }
208
-
209
- if (!workspace) {
210
- throw new Error("Preview workspace could not be inserted or found.");
211
- }
212
-
213
- await ensureOwnerMembership(db, workspace, user);
214
- await ensureWorkspaceSettings(db, workspace);
215
-
216
- return {
217
- workspace: normalizeWorkspaceResult(workspace),
218
- skipped: ""
219
- };
220
- }
221
-
222
- export {
223
- buildWorkspaceBaseSlug,
224
- buildWorkspaceName,
225
- ensurePreviewWorkspace,
226
- normalizeWorkspaceResult
227
- };
@@ -1,149 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { ensurePreviewWorkspace } from "../src/server/previewWorkspaceProvisioning.js";
4
-
5
- function matches(row = {}, criteria = {}) {
6
- return Object.entries(criteria).every(([key, value]) => row[key] === value);
7
- }
8
-
9
- function createMemoryDb(initialTables = {}) {
10
- const tables = {};
11
- for (const [tableName, rows] of Object.entries(initialTables)) {
12
- tables[tableName] = rows.map((row) => ({ ...row }));
13
- }
14
-
15
- function db(tableName) {
16
- const rows = tables[tableName];
17
- if (!rows) {
18
- throw new Error(`Unknown table: ${tableName}`);
19
- }
20
- let criteria = {};
21
- let order = null;
22
-
23
- return {
24
- where(nextCriteria = {}) {
25
- criteria = { ...criteria, ...nextCriteria };
26
- return this;
27
- },
28
- orderBy(column, direction = "asc") {
29
- order = { column, direction };
30
- return this;
31
- },
32
- async first() {
33
- const found = rows.filter((row) => matches(row, criteria));
34
- if (order) {
35
- found.sort((left, right) => {
36
- const leftValue = Number(left[order.column]);
37
- const rightValue = Number(right[order.column]);
38
- const delta = Number.isFinite(leftValue) && Number.isFinite(rightValue)
39
- ? leftValue - rightValue
40
- : String(left[order.column] || "").localeCompare(String(right[order.column] || ""));
41
- return order.direction === "desc" ? delta * -1 : delta;
42
- });
43
- }
44
- return found[0] || null;
45
- },
46
- async insert(record = {}) {
47
- rows.push({
48
- id: record.id || String(rows.length + 1),
49
- ...record
50
- });
51
- },
52
- async update(patch = {}) {
53
- for (const row of rows.filter((entry) => matches(entry, criteria))) {
54
- Object.assign(row, patch);
55
- }
56
- }
57
- };
58
- }
59
-
60
- db.schema = {
61
- async hasTable(tableName) {
62
- return Object.hasOwn(tables, tableName);
63
- }
64
- };
65
- db.tables = tables;
66
- return db;
67
- }
68
-
69
- test("ensurePreviewWorkspace creates a personal workspace, owner membership, and settings", async () => {
70
- const db = createMemoryDb({
71
- workspaces: [],
72
- workspace_memberships: [],
73
- workspace_settings: []
74
- });
75
-
76
- const result = await ensurePreviewWorkspace(
77
- db,
78
- { id: "7" },
79
- { email: "preview@example.test", username: "preview", displayName: "Preview User" },
80
- { appConfig: { tenancyMode: "personal" } }
81
- );
82
-
83
- assert.equal(result.skipped, "");
84
- assert.deepEqual(result.workspace, {
85
- id: "1",
86
- slug: "preview"
87
- });
88
- assert.equal(db.tables.workspaces[0].is_personal, true);
89
- assert.equal(db.tables.workspace_memberships[0].role_sid, "owner");
90
- assert.equal(db.tables.workspace_memberships[0].status, "active");
91
- assert.equal(db.tables.workspace_settings[0].light_primary_color, "#1867C0");
92
- });
93
-
94
- test("ensurePreviewWorkspace repairs an existing owner membership", async () => {
95
- const db = createMemoryDb({
96
- workspaces: [
97
- {
98
- id: "3",
99
- slug: "preview",
100
- owner_user_id: "7",
101
- is_personal: true,
102
- name: "Preview",
103
- avatar_url: ""
104
- }
105
- ],
106
- workspace_memberships: [
107
- {
108
- id: "9",
109
- workspace_id: "3",
110
- user_id: "7",
111
- role_sid: "member",
112
- status: "inactive"
113
- }
114
- ],
115
- workspace_settings: []
116
- });
117
-
118
- const result = await ensurePreviewWorkspace(
119
- db,
120
- { id: "7" },
121
- { email: "preview@example.test", username: "preview" },
122
- { appConfig: { tenancyMode: "personal" } }
123
- );
124
-
125
- assert.equal(result.workspace.id, "3");
126
- assert.equal(db.tables.workspace_memberships[0].role_sid, "owner");
127
- assert.equal(db.tables.workspace_memberships[0].status, "active");
128
- assert.equal(db.tables.workspace_settings.length, 1);
129
- });
130
-
131
- test("ensurePreviewWorkspace skips when workspace tenancy is disabled", async () => {
132
- const db = createMemoryDb({
133
- workspaces: [],
134
- workspace_memberships: [],
135
- workspace_settings: []
136
- });
137
-
138
- const result = await ensurePreviewWorkspace(
139
- db,
140
- { id: "7" },
141
- { email: "preview@example.test" },
142
- { appConfig: { tenancyMode: "none" } }
143
- );
144
-
145
- assert.deepEqual(result, {
146
- workspace: null,
147
- skipped: "workspace tenancy is disabled"
148
- });
149
- });