@elevasis/core 0.8.0 → 0.8.2
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.
- package/dist/test-utils/index.d.ts +3122 -0
- package/dist/test-utils/index.js +386 -0
- package/package.json +6 -1
- package/src/README.md +14 -11
- package/src/__tests__/publish.test.ts +11 -6
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1 -1
- package/src/_gen/__tests__/scaffold-contracts.test.ts +30 -19
- package/src/reference/_generated/contracts.md +1 -1
- package/src/test-utils/README.md +42 -150
- package/src/test-utils/index.ts +10 -11
- package/src/test-utils/published.ts +4 -0
- package/src/test-utils/rls/RLSTestContext.ts +3 -3
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
|
|
4
|
+
// src/test-utils/rls/RLSTestContext.ts
|
|
5
|
+
var RLSTestContext = class {
|
|
6
|
+
adminClient;
|
|
7
|
+
testPrefix;
|
|
8
|
+
createdIds;
|
|
9
|
+
constructor() {
|
|
10
|
+
if (!process.env.SUPABASE_URL) {
|
|
11
|
+
throw new Error("SUPABASE_URL not configured in .env.development");
|
|
12
|
+
}
|
|
13
|
+
if (!process.env.SUPABASE_SERVICE_KEY) {
|
|
14
|
+
throw new Error("SUPABASE_SERVICE_KEY not configured in .env.development");
|
|
15
|
+
}
|
|
16
|
+
if (!process.env.SUPABASE_JWT_SECRET) {
|
|
17
|
+
throw new Error("SUPABASE_JWT_SECRET not configured in .env.development");
|
|
18
|
+
}
|
|
19
|
+
this.adminClient = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_KEY);
|
|
20
|
+
this.testPrefix = `test_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
21
|
+
this.createdIds = {
|
|
22
|
+
users: [],
|
|
23
|
+
organizations: [],
|
|
24
|
+
memberships: [],
|
|
25
|
+
apiKeys: [],
|
|
26
|
+
invitations: [],
|
|
27
|
+
taskSchedules: [],
|
|
28
|
+
commandQueue: [],
|
|
29
|
+
sessions: [],
|
|
30
|
+
sessionMessages: [],
|
|
31
|
+
executionLogs: [],
|
|
32
|
+
executionMetrics: [],
|
|
33
|
+
notifications: [],
|
|
34
|
+
credentials: [],
|
|
35
|
+
activities: []
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a test organization
|
|
40
|
+
*/
|
|
41
|
+
async createOrganization(name) {
|
|
42
|
+
const workosOrgId = `org_${this.testPrefix}_${Math.random().toString(36).substring(7)}`;
|
|
43
|
+
const { data, error } = await this.adminClient.from("organizations").insert({
|
|
44
|
+
workos_org_id: workosOrgId,
|
|
45
|
+
name: `${this.testPrefix}_${name}`,
|
|
46
|
+
is_test: true
|
|
47
|
+
}).select().single();
|
|
48
|
+
if (error) {
|
|
49
|
+
throw new Error(`Failed to create organization: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
this.createdIds.organizations.push(data.id);
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create a test user with a WorkOS user ID
|
|
56
|
+
*/
|
|
57
|
+
async createUser(email, isPlatformAdmin = false) {
|
|
58
|
+
const workosUserId = `user_${this.testPrefix}_${Math.random().toString(36).substring(7)}`;
|
|
59
|
+
const { data, error } = await this.adminClient.from("users").insert({
|
|
60
|
+
workos_user_id: workosUserId,
|
|
61
|
+
email: `${this.testPrefix}_${email}`,
|
|
62
|
+
first_name: "Test",
|
|
63
|
+
last_name: "User",
|
|
64
|
+
is_platform_admin: isPlatformAdmin
|
|
65
|
+
}).select("*").single();
|
|
66
|
+
if (error) {
|
|
67
|
+
throw new Error(`Failed to create user: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
this.createdIds.users.push(data.id);
|
|
70
|
+
return {
|
|
71
|
+
id: data.id,
|
|
72
|
+
workos_user_id: workosUserId,
|
|
73
|
+
email: data.email,
|
|
74
|
+
is_platform_admin: data.is_platform_admin ?? false
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create a pre-provisioned test user (without WorkOS user ID)
|
|
79
|
+
* Used for testing invitation flows where users are created before they sign up
|
|
80
|
+
*/
|
|
81
|
+
async createPreProvisionedUser(email, isPlatformAdmin = false) {
|
|
82
|
+
const { data, error } = await this.adminClient.from("users").insert({
|
|
83
|
+
workos_user_id: null,
|
|
84
|
+
// Key difference: NULL for pre-provisioned
|
|
85
|
+
email: `${this.testPrefix}_${email}`,
|
|
86
|
+
first_name: "Test",
|
|
87
|
+
last_name: "PreProvisioned",
|
|
88
|
+
is_platform_admin: isPlatformAdmin
|
|
89
|
+
}).select("*").single();
|
|
90
|
+
if (error) {
|
|
91
|
+
throw new Error(`Failed to create pre-provisioned user: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
this.createdIds.users.push(data.id);
|
|
94
|
+
return {
|
|
95
|
+
id: data.id,
|
|
96
|
+
email: data.email,
|
|
97
|
+
is_platform_admin: data.is_platform_admin ?? false
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create an organization membership
|
|
102
|
+
*/
|
|
103
|
+
async createMembership(userId, organizationId, role) {
|
|
104
|
+
const workosMembershipId = `om_${this.testPrefix}_${Math.random().toString(36).substring(7)}`;
|
|
105
|
+
const { data, error } = await this.adminClient.from("org_memberships").insert({
|
|
106
|
+
user_id: userId,
|
|
107
|
+
organization_id: organizationId,
|
|
108
|
+
workos_membership_id: workosMembershipId,
|
|
109
|
+
role_slug: role,
|
|
110
|
+
membership_status: "active"
|
|
111
|
+
}).select().single();
|
|
112
|
+
if (error) {
|
|
113
|
+
throw new Error(`Failed to create membership: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
this.createdIds.memberships.push(data.id);
|
|
116
|
+
return {
|
|
117
|
+
id: data.id,
|
|
118
|
+
user_id: data.user_id,
|
|
119
|
+
organization_id: data.organization_id,
|
|
120
|
+
role_slug: data.role_slug
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create a pre-provisioned organization membership (without WorkOS membership ID)
|
|
125
|
+
* Used for testing invitation flows where memberships are created before user accepts
|
|
126
|
+
*/
|
|
127
|
+
async createPreProvisionedMembership(userId, organizationId, role) {
|
|
128
|
+
const { data, error } = await this.adminClient.from("org_memberships").insert({
|
|
129
|
+
user_id: userId,
|
|
130
|
+
organization_id: organizationId,
|
|
131
|
+
workos_membership_id: null,
|
|
132
|
+
// Key difference: NULL for pre-provisioned
|
|
133
|
+
role_slug: role,
|
|
134
|
+
membership_status: "active"
|
|
135
|
+
// Pre-provisioned memberships are active from creation
|
|
136
|
+
}).select().single();
|
|
137
|
+
if (error) {
|
|
138
|
+
throw new Error(`Failed to create pre-provisioned membership: ${error.message}`);
|
|
139
|
+
}
|
|
140
|
+
this.createdIds.memberships.push(data.id);
|
|
141
|
+
return {
|
|
142
|
+
id: data.id,
|
|
143
|
+
user_id: data.user_id,
|
|
144
|
+
organization_id: data.organization_id,
|
|
145
|
+
role_slug: data.role_slug
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Generate a JWT token for a test user
|
|
150
|
+
* Uses Supabase JWT secret so auth.jwt() in RLS policies can decode it
|
|
151
|
+
*/
|
|
152
|
+
generateJWT(workosUserId, email) {
|
|
153
|
+
if (!process.env.SUPABASE_JWT_SECRET) {
|
|
154
|
+
throw new Error("SUPABASE_JWT_SECRET not configured");
|
|
155
|
+
}
|
|
156
|
+
const payload = {
|
|
157
|
+
sub: workosUserId,
|
|
158
|
+
// Supabase RLS policies use auth.jwt() ->> 'sub'
|
|
159
|
+
aud: "authenticated",
|
|
160
|
+
role: "authenticated",
|
|
161
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
162
|
+
exp: Math.floor(Date.now() / 1e3) + 60 * 60,
|
|
163
|
+
// 1 hour expiry
|
|
164
|
+
...email && { email }
|
|
165
|
+
// Add email claim if provided (for pre-provisioned user RLS)
|
|
166
|
+
};
|
|
167
|
+
return jwt.sign(payload, process.env.SUPABASE_JWT_SECRET, {
|
|
168
|
+
algorithm: "HS256"
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Create a Supabase client for a specific user (respects RLS)
|
|
173
|
+
* This client will have the user's JWT token, so RLS policies will apply
|
|
174
|
+
*/
|
|
175
|
+
createUserClient(workosUserId) {
|
|
176
|
+
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_ANON_KEY) {
|
|
177
|
+
throw new Error("Test environment not configured");
|
|
178
|
+
}
|
|
179
|
+
const token = this.generateJWT(workosUserId);
|
|
180
|
+
const client = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
|
|
181
|
+
global: {
|
|
182
|
+
headers: {
|
|
183
|
+
Authorization: `Bearer ${token}`
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return client;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Create a Supabase client for a pre-provisioned user (respects RLS)
|
|
191
|
+
* Uses a dummy workos_user_id but includes the email claim for RLS matching
|
|
192
|
+
* The email claim is what matters for pre-provisioned user RLS policies
|
|
193
|
+
*/
|
|
194
|
+
createPreProvisionedUserClient(email) {
|
|
195
|
+
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_ANON_KEY) {
|
|
196
|
+
throw new Error("Test environment not configured");
|
|
197
|
+
}
|
|
198
|
+
const dummyWorkosUserId = `preprov_${this.testPrefix}_${Math.random().toString(36).substring(7)}`;
|
|
199
|
+
const token = this.generateJWT(dummyWorkosUserId, email);
|
|
200
|
+
const client = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
|
|
201
|
+
global: {
|
|
202
|
+
headers: {
|
|
203
|
+
Authorization: `Bearer ${token}`
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
return client;
|
|
208
|
+
}
|
|
209
|
+
/** Create an organization with an admin user and authenticated client. */
|
|
210
|
+
async createOrgWithAdmin(name, email) {
|
|
211
|
+
const org = await this.createOrganization(name);
|
|
212
|
+
const user = await this.createUser(email, false);
|
|
213
|
+
const membership = await this.createMembership(user.id, org.id, "admin");
|
|
214
|
+
const client = this.createUserClient(user.workos_user_id);
|
|
215
|
+
return { org, user, membership, client };
|
|
216
|
+
}
|
|
217
|
+
/** Create two isolated organizations with admin users for cross-org isolation tests. */
|
|
218
|
+
async createCrossOrgFixture(nameA = "OrgA", nameB = "OrgB") {
|
|
219
|
+
const orgA = await this.createOrganization(nameA);
|
|
220
|
+
const orgB = await this.createOrganization(nameB);
|
|
221
|
+
const userA = await this.createUser(`${nameA.toLowerCase()}Admin@test.com`, false);
|
|
222
|
+
const userB = await this.createUser(`${nameB.toLowerCase()}Admin@test.com`, false);
|
|
223
|
+
const membershipA = await this.createMembership(userA.id, orgA.id, "admin");
|
|
224
|
+
const membershipB = await this.createMembership(userB.id, orgB.id, "admin");
|
|
225
|
+
const clientA = this.createUserClient(userA.workos_user_id);
|
|
226
|
+
const clientB = this.createUserClient(userB.workos_user_id);
|
|
227
|
+
return { orgA, orgB, userA, userB, membershipA, membershipB, clientA, clientB };
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Clean up all test data created during the test run
|
|
231
|
+
* Called in afterAll() to prevent test pollution
|
|
232
|
+
*
|
|
233
|
+
* Cleanup order respects foreign key dependencies (child tables before parent tables):
|
|
234
|
+
*
|
|
235
|
+
* Level 1 (Deepest dependencies):
|
|
236
|
+
* - execution_metrics (FK: execution_logs)
|
|
237
|
+
* - session_messages (FK: sessions)
|
|
238
|
+
*
|
|
239
|
+
* Level 2 (Mid-level dependencies):
|
|
240
|
+
* - execution_logs (FK: sessions, users)
|
|
241
|
+
* - task_schedules (FK: organizations)
|
|
242
|
+
* - command_queue (FK: organizations, users)
|
|
243
|
+
* - notifications (FK: organizations, users)
|
|
244
|
+
* - credentials (FK: organizations, users)
|
|
245
|
+
* - sessions (FK: organizations, users)
|
|
246
|
+
* - activities (FK: organizations)
|
|
247
|
+
*
|
|
248
|
+
* Level 3 (Organization/User dependencies):
|
|
249
|
+
* - invitations (FK: organizations, users)
|
|
250
|
+
* - api_keys (FK: organizations)
|
|
251
|
+
* - memberships (FK: organizations, users)
|
|
252
|
+
*
|
|
253
|
+
* Level 4 (Base tables):
|
|
254
|
+
* - users
|
|
255
|
+
* - organizations
|
|
256
|
+
*/
|
|
257
|
+
async cleanup() {
|
|
258
|
+
const errors = [];
|
|
259
|
+
if (this.createdIds.executionMetrics.length > 0) {
|
|
260
|
+
const { error } = await this.adminClient.from("execution_metrics").delete().in("execution_id", this.createdIds.executionMetrics);
|
|
261
|
+
if (error) {
|
|
262
|
+
errors.push(`Failed to delete execution_metrics: ${error.message}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (this.createdIds.sessionMessages.length > 0) {
|
|
266
|
+
const { error } = await this.adminClient.from("session_messages").delete().in("id", this.createdIds.sessionMessages);
|
|
267
|
+
if (error) {
|
|
268
|
+
errors.push(`Failed to delete session_messages: ${error.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (this.createdIds.executionLogs.length > 0) {
|
|
272
|
+
const { error } = await this.adminClient.from("execution_logs").delete().in("execution_id", this.createdIds.executionLogs);
|
|
273
|
+
if (error) {
|
|
274
|
+
errors.push(`Failed to delete execution_logs: ${error.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (this.createdIds.taskSchedules.length > 0) {
|
|
278
|
+
const { error } = await this.adminClient.from("task_schedules").delete().in("id", this.createdIds.taskSchedules);
|
|
279
|
+
if (error) {
|
|
280
|
+
errors.push(`Failed to delete task_schedules: ${error.message}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (this.createdIds.commandQueue.length > 0) {
|
|
284
|
+
const { error } = await this.adminClient.from("command_queue").delete().in("id", this.createdIds.commandQueue);
|
|
285
|
+
if (error) {
|
|
286
|
+
errors.push(`Failed to delete command_queue: ${error.message}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (this.createdIds.notifications.length > 0) {
|
|
290
|
+
const { error } = await this.adminClient.from("notifications").delete().in("id", this.createdIds.notifications);
|
|
291
|
+
if (error) {
|
|
292
|
+
errors.push(`Failed to delete notifications: ${error.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (this.createdIds.credentials.length > 0) {
|
|
296
|
+
const { error } = await this.adminClient.from("credentials").delete().in("id", this.createdIds.credentials);
|
|
297
|
+
if (error) {
|
|
298
|
+
errors.push(`Failed to delete credentials: ${error.message}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (this.createdIds.sessions.length > 0) {
|
|
302
|
+
const { error } = await this.adminClient.from("sessions").delete().in("session_id", this.createdIds.sessions);
|
|
303
|
+
if (error) {
|
|
304
|
+
errors.push(`Failed to delete sessions: ${error.message}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (this.createdIds.activities.length > 0) {
|
|
308
|
+
const { error } = await this.adminClient.from("activities").delete().in("id", this.createdIds.activities);
|
|
309
|
+
if (error) {
|
|
310
|
+
errors.push(`Failed to delete activities: ${error.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (this.createdIds.invitations.length > 0) {
|
|
314
|
+
const { error } = await this.adminClient.from("org_invitations").delete().in("id", this.createdIds.invitations);
|
|
315
|
+
if (error) {
|
|
316
|
+
errors.push(`Failed to delete invitations: ${error.message}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (this.createdIds.apiKeys.length > 0) {
|
|
320
|
+
const { error } = await this.adminClient.from("api_keys").delete().in("id", this.createdIds.apiKeys);
|
|
321
|
+
if (error) {
|
|
322
|
+
errors.push(`Failed to delete API keys: ${error.message}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (this.createdIds.memberships.length > 0) {
|
|
326
|
+
const { error } = await this.adminClient.from("org_memberships").delete().in("id", this.createdIds.memberships);
|
|
327
|
+
if (error) {
|
|
328
|
+
errors.push(`Failed to delete memberships: ${error.message}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (this.createdIds.users.length > 0) {
|
|
332
|
+
const { error } = await this.adminClient.from("users").delete().in("id", this.createdIds.users);
|
|
333
|
+
if (error) {
|
|
334
|
+
errors.push(`Failed to delete users: ${error.message}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (this.createdIds.organizations.length > 0) {
|
|
338
|
+
const { error } = await this.adminClient.from("organizations").delete().in("id", this.createdIds.organizations);
|
|
339
|
+
if (error) {
|
|
340
|
+
errors.push(`Failed to delete organizations: ${error.message}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (errors.length > 0) {
|
|
344
|
+
console.warn("\n\u26A0\uFE0F Cleanup warnings:", errors.join("\n"));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// src/test-utils/browser-mocks.ts
|
|
350
|
+
function setupMatchMedia() {
|
|
351
|
+
const win = globalThis.window;
|
|
352
|
+
if (typeof win === "undefined") return;
|
|
353
|
+
Object.defineProperty(win, "matchMedia", {
|
|
354
|
+
writable: true,
|
|
355
|
+
value: (query) => ({
|
|
356
|
+
matches: false,
|
|
357
|
+
media: query,
|
|
358
|
+
onchange: null,
|
|
359
|
+
addListener: () => {
|
|
360
|
+
},
|
|
361
|
+
removeListener: () => {
|
|
362
|
+
},
|
|
363
|
+
addEventListener: () => {
|
|
364
|
+
},
|
|
365
|
+
removeEventListener: () => {
|
|
366
|
+
},
|
|
367
|
+
dispatchEvent: () => true
|
|
368
|
+
})
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function setupResizeObserver() {
|
|
372
|
+
globalThis.ResizeObserver = class ResizeObserver {
|
|
373
|
+
observe() {
|
|
374
|
+
}
|
|
375
|
+
unobserve() {
|
|
376
|
+
}
|
|
377
|
+
disconnect() {
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function setupBrowserMocks() {
|
|
382
|
+
setupMatchMedia();
|
|
383
|
+
setupResizeObserver();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export { RLSTestContext, setupBrowserMocks, setupMatchMedia, setupResizeObserver };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elevasis/core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Minimal shared constants across Elevasis monorepo",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"import": "./dist/index.js"
|
|
18
18
|
},
|
|
19
|
+
"./test-utils": {
|
|
20
|
+
"types": "./dist/test-utils/index.d.ts",
|
|
21
|
+
"import": "./dist/test-utils/index.js"
|
|
22
|
+
},
|
|
19
23
|
"./organization-model": {
|
|
20
24
|
"types": "./dist/organization-model/index.d.ts",
|
|
21
25
|
"import": "./dist/organization-model/index.js"
|
|
@@ -43,6 +47,7 @@
|
|
|
43
47
|
"@googleapis/gmail": "^15.0.0",
|
|
44
48
|
"@googleapis/sheets": "^12.0.0",
|
|
45
49
|
"@sentry/node": "^8.55.0",
|
|
50
|
+
"@supabase/supabase-js": "^2.80.0",
|
|
46
51
|
"@workos-inc/node": "^7.69.2",
|
|
47
52
|
"croner": "^10.0.1",
|
|
48
53
|
"date-fns-tz": "^3.2.0",
|
package/src/README.md
CHANGED
|
@@ -6,17 +6,20 @@ This package is the source of truth for shared types, schemas, and contract help
|
|
|
6
6
|
|
|
7
7
|
## Import Rules
|
|
8
8
|
|
|
9
|
-
- Use `@elevasis/core` (root export) for browser-safe shared types and schemas.
|
|
10
|
-
- Use `@elevasis/core/organization-model` for the semantic contract layer.
|
|
11
|
-
-
|
|
12
|
-
-
|
|
9
|
+
- Use `@elevasis/core` (root export) for browser-safe shared types and schemas.
|
|
10
|
+
- Use `@elevasis/core/organization-model` for the semantic contract layer.
|
|
11
|
+
- Use `@elevasis/core/entities` for the published base entity contracts.
|
|
12
|
+
- Use `@elevasis/core/test-utils` for shared test fixtures, mocks, and test helpers.
|
|
13
|
+
- Paths like `@elevasis/core/server` and `@elevasis/core/platform` are internal monorepo paths (`@repo/core/...`) and are NOT available to external consumers.
|
|
13
14
|
|
|
14
15
|
## Published Surface Groups
|
|
15
16
|
|
|
16
|
-
The published `@elevasis/core` npm package exposes
|
|
17
|
-
|
|
18
|
-
- `.` (`@elevasis/core`) - browser-safe shared types, schemas, and constants.
|
|
19
|
-
- `./organization-model` (`@elevasis/core/organization-model`) - the semantic contract layer for CRM, lead gen, delivery, features, branding, and navigation.
|
|
17
|
+
The published `@elevasis/core` npm package exposes these subpaths:
|
|
18
|
+
|
|
19
|
+
- `.` (`@elevasis/core`) - browser-safe shared types, schemas, and constants.
|
|
20
|
+
- `./organization-model` (`@elevasis/core/organization-model`) - the semantic contract layer for CRM, lead gen, delivery, features, branding, and navigation.
|
|
21
|
+
- `./entities` (`@elevasis/core/entities`) - published base entity contracts generic over project metadata extensions.
|
|
22
|
+
- `./test-utils` (`@elevasis/core/test-utils`) - test fixtures, mocks, and helpers for downstream automated tests.
|
|
20
23
|
|
|
21
24
|
Within the monorepo, the internal `@repo/core` package exposes additional subpaths for use by `apps/` and other packages:
|
|
22
25
|
|
|
@@ -30,9 +33,9 @@ Within the monorepo, the internal `@repo/core` package exposes additional subpat
|
|
|
30
33
|
- `@repo/core/integrations/...` - OAuth and credential contracts.
|
|
31
34
|
- `@repo/core/projects/api-schemas` - project management request and response schemas.
|
|
32
35
|
- `@repo/core/content` - published content metadata types.
|
|
33
|
-
- `@repo/core/test-utils` - test fixtures and mocks
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
- `@repo/core/test-utils` - source of the published test fixtures and mocks surface.
|
|
37
|
+
|
|
38
|
+
Other `@repo/core/*` subpaths remain monorepo-only unless they are explicitly listed above in the published `@elevasis/core` surface.
|
|
36
39
|
|
|
37
40
|
## When To Read Deeper
|
|
38
41
|
|
|
@@ -10,9 +10,14 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
describe('core publish surface', () => {
|
|
14
|
-
it('publishes the curated @elevasis/core organization-model wrapper', () => {
|
|
15
|
-
expect(packageJson.publishConfig?.name).toBe('@elevasis/core')
|
|
16
|
-
expect(Object.keys(packageJson.publishConfig?.exports ?? {})).toEqual([
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
describe('core publish surface', () => {
|
|
14
|
+
it('publishes the curated @elevasis/core organization-model wrapper', () => {
|
|
15
|
+
expect(packageJson.publishConfig?.name).toBe('@elevasis/core')
|
|
16
|
+
expect(Object.keys(packageJson.publishConfig?.exports ?? {})).toEqual([
|
|
17
|
+
'.',
|
|
18
|
+
'./test-utils',
|
|
19
|
+
'./organization-model',
|
|
20
|
+
'./entities'
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Auto-generated on
|
|
1
|
+
<!-- Auto-generated on YYYY-MM-DD by scripts/monorepo/generate-scaffold-contracts.js -->
|
|
2
2
|
---
|
|
3
3
|
title: Reference Contracts
|
|
4
4
|
description: Auto-generated TypeScript contracts for SDK consumers. Do not edit manually.
|
|
@@ -9,16 +9,26 @@
|
|
|
9
9
|
* if the output changes without an intentional snapshot update.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { readFileSync } from 'node:fs'
|
|
13
|
-
import { resolve } from 'node:path'
|
|
14
|
-
import { describe, it, expect } from 'vitest'
|
|
12
|
+
import { readFileSync } from 'node:fs'
|
|
13
|
+
import { resolve } from 'node:path'
|
|
14
|
+
import { describe, it, expect } from 'vitest'
|
|
15
15
|
|
|
16
16
|
/** Monorepo root relative to packages/core/src/_gen/__tests__/ */
|
|
17
|
-
const ROOT = resolve(import.meta.dirname, '..', '..', '..', '..', '..')
|
|
17
|
+
const ROOT = resolve(import.meta.dirname, '..', '..', '..', '..', '..')
|
|
18
|
+
|
|
19
|
+
const OUTPUT_PATH = resolve(ROOT, 'packages/core/src/reference/_generated/contracts.md')
|
|
20
|
+
const SNAPSHOT_PATH = resolve(import.meta.dirname, '__snapshots__', 'contracts.md.snap')
|
|
21
|
+
|
|
22
|
+
function normalizeSnapshotContent(content: string) {
|
|
23
|
+
return content
|
|
24
|
+
.replace(/\r\n/g, '\n')
|
|
25
|
+
.replace(
|
|
26
|
+
/<!-- Auto-generated on \d{4}-\d{2}-\d{2} by scripts\/monorepo\/generate-scaffold-contracts\.js -->/,
|
|
27
|
+
'<!-- Auto-generated on YYYY-MM-DD by scripts/monorepo/generate-scaffold-contracts.js -->'
|
|
28
|
+
)
|
|
29
|
+
}
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
describe('scaffold-contracts generator', () => {
|
|
31
|
+
describe('scaffold-contracts generator', () => {
|
|
22
32
|
it('output file exists and has content', () => {
|
|
23
33
|
// The generator must have been run (either manually or by CI gen step).
|
|
24
34
|
// This test validates the committed artifact — it does NOT re-run the generator
|
|
@@ -36,18 +46,19 @@ describe('scaffold-contracts generator', () => {
|
|
|
36
46
|
expect(content.length).toBeGreaterThan(0)
|
|
37
47
|
})
|
|
38
48
|
|
|
39
|
-
it('output file matches stored snapshot',
|
|
40
|
-
let content: string
|
|
41
|
-
try {
|
|
42
|
-
content = readFileSync(OUTPUT_PATH, 'utf8')
|
|
49
|
+
it('output file matches stored snapshot', () => {
|
|
50
|
+
let content: string
|
|
51
|
+
try {
|
|
52
|
+
content = readFileSync(OUTPUT_PATH, 'utf8')
|
|
43
53
|
} catch {
|
|
44
54
|
throw new Error(
|
|
45
55
|
`Generated file not found: ${OUTPUT_PATH}\n` +
|
|
46
|
-
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
})
|
|
56
|
+
`Run "pnpm scaffold:generate" first to produce the artifact before snapshotting.`
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const snapshot = readFileSync(SNAPSHOT_PATH, 'utf8')
|
|
61
|
+
|
|
62
|
+
expect(normalizeSnapshotContent(content)).toBe(normalizeSnapshotContent(snapshot))
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Auto-generated on 2026-04-
|
|
1
|
+
<!-- Auto-generated on 2026-04-23 by scripts/monorepo/generate-scaffold-contracts.js -->
|
|
2
2
|
---
|
|
3
3
|
title: Reference Contracts
|
|
4
4
|
description: Auto-generated TypeScript contracts for SDK consumers. Do not edit manually.
|