@sdods/db 0.2.0
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/LICENSE +17 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/col.d.ts +32 -0
- package/dist/col.js +58 -0
- package/dist/create-db.d.ts +14 -0
- package/dist/create-db.js +58 -0
- package/dist/driver.d.ts +10 -0
- package/dist/driver.js +16 -0
- package/dist/ids.d.ts +2 -0
- package/dist/ids.js +2 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +22 -0
- package/dist/ingest/attachments.d.ts +40 -0
- package/dist/ingest/attachments.js +154 -0
- package/dist/ingest/cucumber-ingest.d.ts +40 -0
- package/dist/ingest/cucumber-ingest.js +543 -0
- package/dist/ingest/finalize.d.ts +28 -0
- package/dist/ingest/finalize.js +132 -0
- package/dist/ingest/index.d.ts +26 -0
- package/dist/ingest/index.js +193 -0
- package/dist/ingest/message-index.d.ts +64 -0
- package/dist/ingest/message-index.js +146 -0
- package/dist/ingest/parse-ndjson.d.ts +10 -0
- package/dist/ingest/parse-ndjson.js +35 -0
- package/dist/ingest/pw-json-ingest.d.ts +14 -0
- package/dist/ingest/pw-json-ingest.js +259 -0
- package/dist/ingest/runner-json-ingest.d.ts +14 -0
- package/dist/ingest/runner-json-ingest.js +259 -0
- package/dist/ingest/types.d.ts +62 -0
- package/dist/ingest/types.js +89 -0
- package/dist/insights.d.ts +62 -0
- package/dist/insights.js +184 -0
- package/dist/migrate.d.ts +30 -0
- package/dist/migrate.js +114 -0
- package/dist/migrations/0001_platform_init.d.ts +5 -0
- package/dist/migrations/0001_platform_init.js +48 -0
- package/dist/migrations/0002_results.d.ts +5 -0
- package/dist/migrations/0002_results.js +124 -0
- package/dist/migrations/0003_improvement.d.ts +5 -0
- package/dist/migrations/0003_improvement.js +56 -0
- package/dist/migrations/0004_test_data.d.ts +5 -0
- package/dist/migrations/0004_test_data.js +48 -0
- package/dist/migrations/0005_integrations_agents_audit.d.ts +5 -0
- package/dist/migrations/0005_integrations_agents_audit.js +100 -0
- package/dist/migrations/0006_schedules.d.ts +5 -0
- package/dist/migrations/0006_schedules.js +34 -0
- package/dist/migrations/0007_hierarchy.d.ts +5 -0
- package/dist/migrations/0007_hierarchy.js +76 -0
- package/dist/migrations/0008_runner_naming.d.ts +14 -0
- package/dist/migrations/0008_runner_naming.js +35 -0
- package/dist/migrations/helpers.d.ts +15 -0
- package/dist/migrations/helpers.js +30 -0
- package/dist/migrations/index.d.ts +6 -0
- package/dist/migrations/index.js +22 -0
- package/dist/repos/auth.d.ts +109 -0
- package/dist/repos/auth.js +258 -0
- package/dist/repos/hierarchy.d.ts +151 -0
- package/dist/repos/hierarchy.js +465 -0
- package/dist/repos/improvement.d.ts +84 -0
- package/dist/repos/improvement.js +242 -0
- package/dist/repos/index.d.ts +8 -0
- package/dist/repos/index.js +8 -0
- package/dist/repos/platform.d.ts +250 -0
- package/dist/repos/platform.js +429 -0
- package/dist/repos/projects.d.ts +87 -0
- package/dist/repos/projects.js +118 -0
- package/dist/repos/runs.d.ts +306 -0
- package/dist/repos/runs.js +321 -0
- package/dist/repos/test-data.d.ts +84 -0
- package/dist/repos/test-data.js +291 -0
- package/dist/schema.d.ts +514 -0
- package/dist/schema.js +37 -0
- package/dist/scopes.d.ts +2 -0
- package/dist/scopes.js +2 -0
- package/dist/switch/export.d.ts +15 -0
- package/dist/switch/export.js +67 -0
- package/dist/switch/import.d.ts +16 -0
- package/dist/switch/import.js +106 -0
- package/dist/switch/index.d.ts +5 -0
- package/dist/switch/index.js +5 -0
- package/dist/switch/prune.d.ts +19 -0
- package/dist/switch/prune.js +50 -0
- package/dist/switch/switch.d.ts +34 -0
- package/dist/switch/switch.js +133 -0
- package/dist/test-data.d.ts +22 -0
- package/dist/test-data.js +79 -0
- package/package.json +50 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { effectiveWorkspaceRole, } from '@sdods/contracts/scopes';
|
|
2
|
+
import { enc, nowIso, readBool, readJson, readTs } from '../col.js';
|
|
3
|
+
import { newId } from '../ids.js';
|
|
4
|
+
import { upsertProject } from './projects.js';
|
|
5
|
+
// ── organizations / workspaces ────────────────────────────────────────────
|
|
6
|
+
export async function upsertOrganization(db, input) {
|
|
7
|
+
const existing = await db
|
|
8
|
+
.selectFrom('organizations')
|
|
9
|
+
.select(['id'])
|
|
10
|
+
.where('slug', '=', input.slug)
|
|
11
|
+
.executeTakeFirst();
|
|
12
|
+
const values = {
|
|
13
|
+
name: input.name,
|
|
14
|
+
description: input.description ?? null,
|
|
15
|
+
url: input.url ?? null,
|
|
16
|
+
updated_at: nowIso(),
|
|
17
|
+
};
|
|
18
|
+
if (existing) {
|
|
19
|
+
await db.updateTable('organizations').set(values).where('id', '=', existing.id).execute();
|
|
20
|
+
return existing.id;
|
|
21
|
+
}
|
|
22
|
+
const id = newId();
|
|
23
|
+
await db
|
|
24
|
+
.insertInto('organizations')
|
|
25
|
+
.values({ id, slug: input.slug, created_at: nowIso(), ...values })
|
|
26
|
+
.execute();
|
|
27
|
+
return id;
|
|
28
|
+
}
|
|
29
|
+
export async function upsertWorkspace(db, input) {
|
|
30
|
+
const existing = await db
|
|
31
|
+
.selectFrom('workspaces')
|
|
32
|
+
.select(['id'])
|
|
33
|
+
.where('organization_id', '=', input.organizationId)
|
|
34
|
+
.where('slug', '=', input.slug)
|
|
35
|
+
.executeTakeFirst();
|
|
36
|
+
const values = { name: input.name, description: input.description ?? null, updated_at: nowIso() };
|
|
37
|
+
if (existing) {
|
|
38
|
+
await db.updateTable('workspaces').set(values).where('id', '=', existing.id).execute();
|
|
39
|
+
return existing.id;
|
|
40
|
+
}
|
|
41
|
+
const id = newId();
|
|
42
|
+
await db
|
|
43
|
+
.insertInto('workspaces')
|
|
44
|
+
.values({
|
|
45
|
+
id,
|
|
46
|
+
organization_id: input.organizationId,
|
|
47
|
+
slug: input.slug,
|
|
48
|
+
created_at: nowIso(),
|
|
49
|
+
...values,
|
|
50
|
+
})
|
|
51
|
+
.execute();
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
54
|
+
export async function listOrganizations(db) {
|
|
55
|
+
const rows = await db.selectFrom('organizations').selectAll().orderBy('slug').execute();
|
|
56
|
+
return rows.map((o) => ({
|
|
57
|
+
id: o.id,
|
|
58
|
+
slug: o.slug,
|
|
59
|
+
name: o.name,
|
|
60
|
+
description: o.description,
|
|
61
|
+
url: o.url,
|
|
62
|
+
createdAt: readTs(o.created_at),
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
export async function listWorkspaces(db, organizationId) {
|
|
66
|
+
let q = db
|
|
67
|
+
.selectFrom('workspaces')
|
|
68
|
+
.innerJoin('organizations', 'organizations.id', 'workspaces.organization_id')
|
|
69
|
+
.selectAll('workspaces')
|
|
70
|
+
.select('organizations.slug as org_slug')
|
|
71
|
+
.orderBy('workspaces.slug');
|
|
72
|
+
if (organizationId)
|
|
73
|
+
q = q.where('workspaces.organization_id', '=', organizationId);
|
|
74
|
+
const rows = await q.execute();
|
|
75
|
+
return rows.map(mapWorkspace);
|
|
76
|
+
}
|
|
77
|
+
export async function getWorkspaceBySlug(db, orgSlug, slug) {
|
|
78
|
+
const row = await db
|
|
79
|
+
.selectFrom('workspaces')
|
|
80
|
+
.innerJoin('organizations', 'organizations.id', 'workspaces.organization_id')
|
|
81
|
+
.selectAll('workspaces')
|
|
82
|
+
.select('organizations.slug as org_slug')
|
|
83
|
+
.where('organizations.slug', '=', orgSlug)
|
|
84
|
+
.where('workspaces.slug', '=', slug)
|
|
85
|
+
.executeTakeFirst();
|
|
86
|
+
return row ? mapWorkspace(row) : null;
|
|
87
|
+
}
|
|
88
|
+
function mapWorkspace(w) {
|
|
89
|
+
return {
|
|
90
|
+
id: w.id,
|
|
91
|
+
organizationId: w.organization_id,
|
|
92
|
+
organizationSlug: w.org_slug,
|
|
93
|
+
slug: w.slug,
|
|
94
|
+
name: w.name,
|
|
95
|
+
description: w.description,
|
|
96
|
+
createdAt: readTs(w.created_at),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// ── memberships ───────────────────────────────────────────────────────────
|
|
100
|
+
export async function addOrgMember(db, organizationId, userId, role) {
|
|
101
|
+
const existing = await db
|
|
102
|
+
.selectFrom('org_members')
|
|
103
|
+
.select(['id'])
|
|
104
|
+
.where('organization_id', '=', organizationId)
|
|
105
|
+
.where('user_id', '=', userId)
|
|
106
|
+
.executeTakeFirst();
|
|
107
|
+
if (existing) {
|
|
108
|
+
await db.updateTable('org_members').set({ role }).where('id', '=', existing.id).execute();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await db
|
|
112
|
+
.insertInto('org_members')
|
|
113
|
+
.values({
|
|
114
|
+
id: newId(),
|
|
115
|
+
organization_id: organizationId,
|
|
116
|
+
user_id: userId,
|
|
117
|
+
role,
|
|
118
|
+
created_at: nowIso(),
|
|
119
|
+
})
|
|
120
|
+
.execute();
|
|
121
|
+
}
|
|
122
|
+
export async function removeOrgMember(db, organizationId, userId) {
|
|
123
|
+
await db
|
|
124
|
+
.deleteFrom('org_members')
|
|
125
|
+
.where('organization_id', '=', organizationId)
|
|
126
|
+
.where('user_id', '=', userId)
|
|
127
|
+
.execute();
|
|
128
|
+
}
|
|
129
|
+
export async function addWorkspaceMember(db, workspaceId, userId, role) {
|
|
130
|
+
const existing = await db
|
|
131
|
+
.selectFrom('workspace_members')
|
|
132
|
+
.select(['id'])
|
|
133
|
+
.where('workspace_id', '=', workspaceId)
|
|
134
|
+
.where('user_id', '=', userId)
|
|
135
|
+
.executeTakeFirst();
|
|
136
|
+
if (existing) {
|
|
137
|
+
await db.updateTable('workspace_members').set({ role }).where('id', '=', existing.id).execute();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await db
|
|
141
|
+
.insertInto('workspace_members')
|
|
142
|
+
.values({ id: newId(), workspace_id: workspaceId, user_id: userId, role, created_at: nowIso() })
|
|
143
|
+
.execute();
|
|
144
|
+
}
|
|
145
|
+
export async function removeWorkspaceMember(db, workspaceId, userId) {
|
|
146
|
+
await db
|
|
147
|
+
.deleteFrom('workspace_members')
|
|
148
|
+
.where('workspace_id', '=', workspaceId)
|
|
149
|
+
.where('user_id', '=', userId)
|
|
150
|
+
.execute();
|
|
151
|
+
}
|
|
152
|
+
/** `setRole` for either level. */
|
|
153
|
+
export async function setRole(db, target) {
|
|
154
|
+
if ('organizationId' in target)
|
|
155
|
+
return addOrgMember(db, target.organizationId, target.userId, target.role);
|
|
156
|
+
return addWorkspaceMember(db, target.workspaceId, target.userId, target.role);
|
|
157
|
+
}
|
|
158
|
+
export async function listOrgMembers(db, organizationId) {
|
|
159
|
+
const rows = await db
|
|
160
|
+
.selectFrom('org_members')
|
|
161
|
+
.innerJoin('users', 'users.id', 'org_members.user_id')
|
|
162
|
+
.select([
|
|
163
|
+
'org_members.id as id',
|
|
164
|
+
'org_members.user_id as user_id',
|
|
165
|
+
'users.username as username',
|
|
166
|
+
'org_members.role as role',
|
|
167
|
+
'org_members.created_at as created_at',
|
|
168
|
+
])
|
|
169
|
+
.where('org_members.organization_id', '=', organizationId)
|
|
170
|
+
.orderBy('users.username')
|
|
171
|
+
.execute();
|
|
172
|
+
return rows.map((r) => ({
|
|
173
|
+
id: r.id,
|
|
174
|
+
userId: r.user_id,
|
|
175
|
+
username: r.username,
|
|
176
|
+
role: r.role,
|
|
177
|
+
createdAt: readTs(r.created_at),
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
export async function listWorkspaceMembers(db, workspaceId) {
|
|
181
|
+
const rows = await db
|
|
182
|
+
.selectFrom('workspace_members')
|
|
183
|
+
.innerJoin('users', 'users.id', 'workspace_members.user_id')
|
|
184
|
+
.select([
|
|
185
|
+
'workspace_members.id as id',
|
|
186
|
+
'workspace_members.user_id as user_id',
|
|
187
|
+
'users.username as username',
|
|
188
|
+
'workspace_members.role as role',
|
|
189
|
+
'workspace_members.created_at as created_at',
|
|
190
|
+
])
|
|
191
|
+
.where('workspace_members.workspace_id', '=', workspaceId)
|
|
192
|
+
.orderBy('users.username')
|
|
193
|
+
.execute();
|
|
194
|
+
return rows.map((r) => ({
|
|
195
|
+
id: r.id,
|
|
196
|
+
userId: r.user_id,
|
|
197
|
+
username: r.username,
|
|
198
|
+
role: r.role,
|
|
199
|
+
createdAt: readTs(r.created_at),
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
export async function listUserOrgs(db, userId) {
|
|
203
|
+
const rows = await db
|
|
204
|
+
.selectFrom('org_members')
|
|
205
|
+
.innerJoin('organizations', 'organizations.id', 'org_members.organization_id')
|
|
206
|
+
.select([
|
|
207
|
+
'organizations.id as id',
|
|
208
|
+
'organizations.slug as slug',
|
|
209
|
+
'organizations.name as name',
|
|
210
|
+
'org_members.role as role',
|
|
211
|
+
])
|
|
212
|
+
.where('org_members.user_id', '=', userId)
|
|
213
|
+
.orderBy('organizations.slug')
|
|
214
|
+
.execute();
|
|
215
|
+
return rows.map((r) => ({ id: r.id, slug: r.slug, name: r.name, role: r.role }));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Every workspace the user can see with the effective role (max of explicit workspace role and the
|
|
219
|
+
* role implied by org membership; platform admins see everything as admin).
|
|
220
|
+
*/
|
|
221
|
+
export async function listUserWorkspaces(db, userId, platformRole) {
|
|
222
|
+
const orgRoles = new Map();
|
|
223
|
+
for (const o of await listUserOrgs(db, userId))
|
|
224
|
+
orgRoles.set(o.id, o.role);
|
|
225
|
+
const wsRoles = new Map();
|
|
226
|
+
const memberships = await db
|
|
227
|
+
.selectFrom('workspace_members')
|
|
228
|
+
.select(['workspace_id', 'role'])
|
|
229
|
+
.where('user_id', '=', userId)
|
|
230
|
+
.execute();
|
|
231
|
+
for (const m of memberships)
|
|
232
|
+
wsRoles.set(m.workspace_id, m.role);
|
|
233
|
+
const all = await listWorkspaces(db);
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const w of all) {
|
|
236
|
+
const role = effectiveWorkspaceRole({
|
|
237
|
+
platformRole,
|
|
238
|
+
orgRole: orgRoles.get(w.organizationId),
|
|
239
|
+
workspaceRole: wsRoles.get(w.id),
|
|
240
|
+
});
|
|
241
|
+
if (role)
|
|
242
|
+
out.push({ ...w, role });
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
export async function effectiveRoleForWorkspace(db, userId, workspaceId, platformRole) {
|
|
247
|
+
const ws = await db
|
|
248
|
+
.selectFrom('workspaces')
|
|
249
|
+
.select(['organization_id'])
|
|
250
|
+
.where('id', '=', workspaceId)
|
|
251
|
+
.executeTakeFirst();
|
|
252
|
+
if (!ws)
|
|
253
|
+
return null;
|
|
254
|
+
const org = await db
|
|
255
|
+
.selectFrom('org_members')
|
|
256
|
+
.select(['role'])
|
|
257
|
+
.where('organization_id', '=', ws.organization_id)
|
|
258
|
+
.where('user_id', '=', userId)
|
|
259
|
+
.executeTakeFirst();
|
|
260
|
+
const m = await db
|
|
261
|
+
.selectFrom('workspace_members')
|
|
262
|
+
.select(['role'])
|
|
263
|
+
.where('workspace_id', '=', workspaceId)
|
|
264
|
+
.where('user_id', '=', userId)
|
|
265
|
+
.executeTakeFirst();
|
|
266
|
+
return effectiveWorkspaceRole({
|
|
267
|
+
platformRole,
|
|
268
|
+
orgRole: org?.role,
|
|
269
|
+
workspaceRole: m?.role,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/** Grant `owner` on every organization that has no owner yet (admin bootstrap). */
|
|
273
|
+
export async function bootstrapOwner(db, userId) {
|
|
274
|
+
const orgs = await db.selectFrom('organizations').select(['id', 'slug']).execute();
|
|
275
|
+
const granted = [];
|
|
276
|
+
for (const o of orgs) {
|
|
277
|
+
const owner = await db
|
|
278
|
+
.selectFrom('org_members')
|
|
279
|
+
.select(['id'])
|
|
280
|
+
.where('organization_id', '=', o.id)
|
|
281
|
+
.where('role', '=', 'owner')
|
|
282
|
+
.executeTakeFirst();
|
|
283
|
+
if (owner)
|
|
284
|
+
continue;
|
|
285
|
+
await addOrgMember(db, o.id, userId, 'owner');
|
|
286
|
+
granted.push(o.slug);
|
|
287
|
+
}
|
|
288
|
+
return granted;
|
|
289
|
+
}
|
|
290
|
+
// ── modules / processes ───────────────────────────────────────────────────
|
|
291
|
+
export async function upsertModule(db, projectId, m) {
|
|
292
|
+
const existing = await db
|
|
293
|
+
.selectFrom('modules')
|
|
294
|
+
.select(['id'])
|
|
295
|
+
.where('project_id', '=', projectId)
|
|
296
|
+
.where('name', '=', m.name)
|
|
297
|
+
.executeTakeFirst();
|
|
298
|
+
const values = {
|
|
299
|
+
title: m.title ?? null,
|
|
300
|
+
description: m.description ?? null,
|
|
301
|
+
path: m.path ?? m.name,
|
|
302
|
+
layers_json: enc.json(m.layers ?? []),
|
|
303
|
+
testing_types_json: enc.json(m.testingTypes ?? []),
|
|
304
|
+
tags_json: enc.json(m.tags ?? []),
|
|
305
|
+
owner: m.owner ?? null,
|
|
306
|
+
jira_component: m.jiraComponent ?? null,
|
|
307
|
+
routes_json: enc.json(m.routes ?? []),
|
|
308
|
+
endpoints_json: enc.json(m.endpoints ?? []),
|
|
309
|
+
updated_at: nowIso(),
|
|
310
|
+
};
|
|
311
|
+
if (existing) {
|
|
312
|
+
await db.updateTable('modules').set(values).where('id', '=', existing.id).execute();
|
|
313
|
+
return existing.id;
|
|
314
|
+
}
|
|
315
|
+
const id = newId();
|
|
316
|
+
await db
|
|
317
|
+
.insertInto('modules')
|
|
318
|
+
.values({ id, project_id: projectId, name: m.name, created_at: nowIso(), ...values })
|
|
319
|
+
.execute();
|
|
320
|
+
return id;
|
|
321
|
+
}
|
|
322
|
+
export async function listModules(db, projectId) {
|
|
323
|
+
const rows = await db
|
|
324
|
+
.selectFrom('modules')
|
|
325
|
+
.selectAll()
|
|
326
|
+
.where('project_id', '=', projectId)
|
|
327
|
+
.orderBy('name')
|
|
328
|
+
.execute();
|
|
329
|
+
return rows.map((r) => ({
|
|
330
|
+
id: r.id,
|
|
331
|
+
name: r.name,
|
|
332
|
+
title: r.title,
|
|
333
|
+
description: r.description,
|
|
334
|
+
path: r.path,
|
|
335
|
+
layers: readJson(r.layers_json) ?? [],
|
|
336
|
+
testingTypes: readJson(r.testing_types_json) ?? [],
|
|
337
|
+
tags: readJson(r.tags_json) ?? [],
|
|
338
|
+
owner: r.owner,
|
|
339
|
+
jiraComponent: r.jira_component,
|
|
340
|
+
routes: readJson(r.routes_json) ?? [],
|
|
341
|
+
endpoints: readJson(r.endpoints_json) ?? [],
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
export async function upsertProcess(db, driver, scope, p) {
|
|
345
|
+
let q = db.selectFrom('processes').select(['id']).where('name', '=', p.name);
|
|
346
|
+
q = scope.projectId
|
|
347
|
+
? q.where('project_id', '=', scope.projectId)
|
|
348
|
+
: q.where('project_id', 'is', null).where('workspace_id', '=', scope.workspaceId ?? null);
|
|
349
|
+
const existing = await q.executeTakeFirst();
|
|
350
|
+
const { name: _n, title, description, trigger, ...config } = p;
|
|
351
|
+
const values = {
|
|
352
|
+
title: title ?? null,
|
|
353
|
+
description: description ?? null,
|
|
354
|
+
trigger: trigger ?? 'manual',
|
|
355
|
+
config_json: enc.json(config),
|
|
356
|
+
enabled: enc.bool(driver, true),
|
|
357
|
+
updated_at: nowIso(),
|
|
358
|
+
};
|
|
359
|
+
if (existing) {
|
|
360
|
+
await db.updateTable('processes').set(values).where('id', '=', existing.id).execute();
|
|
361
|
+
return existing.id;
|
|
362
|
+
}
|
|
363
|
+
const id = newId();
|
|
364
|
+
await db
|
|
365
|
+
.insertInto('processes')
|
|
366
|
+
.values({
|
|
367
|
+
id,
|
|
368
|
+
project_id: scope.projectId ?? null,
|
|
369
|
+
workspace_id: scope.workspaceId ?? null,
|
|
370
|
+
name: p.name,
|
|
371
|
+
last_run_id: null,
|
|
372
|
+
last_status: null,
|
|
373
|
+
created_at: nowIso(),
|
|
374
|
+
...values,
|
|
375
|
+
})
|
|
376
|
+
.execute();
|
|
377
|
+
return id;
|
|
378
|
+
}
|
|
379
|
+
export async function listProcesses(db, scope = {}) {
|
|
380
|
+
let q = db.selectFrom('processes').selectAll().orderBy('name');
|
|
381
|
+
if (scope.projectId)
|
|
382
|
+
q = q.where('project_id', '=', scope.projectId);
|
|
383
|
+
if (scope.workspaceId)
|
|
384
|
+
q = q.where('workspace_id', '=', scope.workspaceId);
|
|
385
|
+
const rows = await q.execute();
|
|
386
|
+
return rows.map((r) => ({
|
|
387
|
+
id: r.id,
|
|
388
|
+
projectId: r.project_id,
|
|
389
|
+
workspaceId: r.workspace_id,
|
|
390
|
+
name: r.name,
|
|
391
|
+
title: r.title,
|
|
392
|
+
description: r.description,
|
|
393
|
+
trigger: r.trigger,
|
|
394
|
+
config: readJson(r.config_json) ?? {},
|
|
395
|
+
enabled: readBool(r.enabled),
|
|
396
|
+
lastRunId: r.last_run_id,
|
|
397
|
+
lastStatus: r.last_status,
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
export async function updateProcessState(db, id, patch) {
|
|
401
|
+
await db
|
|
402
|
+
.updateTable('processes')
|
|
403
|
+
.set({
|
|
404
|
+
last_run_id: patch.lastRunId ?? null,
|
|
405
|
+
last_status: patch.lastStatus ?? null,
|
|
406
|
+
updated_at: nowIso(),
|
|
407
|
+
})
|
|
408
|
+
.where('id', '=', id)
|
|
409
|
+
.execute();
|
|
410
|
+
}
|
|
411
|
+
/** Upsert organization → workspaces → projects → modules/processes from the yaml files. */
|
|
412
|
+
export async function syncHierarchy(db, driver, input) {
|
|
413
|
+
const result = {
|
|
414
|
+
organizationId: null,
|
|
415
|
+
workspaces: {},
|
|
416
|
+
projects: {},
|
|
417
|
+
modules: 0,
|
|
418
|
+
processes: 0,
|
|
419
|
+
};
|
|
420
|
+
const wf = input.workspaceFile ?? null;
|
|
421
|
+
let defaultWorkspaceId = null;
|
|
422
|
+
if (wf) {
|
|
423
|
+
result.organizationId = await upsertOrganization(db, wf.organization);
|
|
424
|
+
for (const ws of wf.workspaces) {
|
|
425
|
+
const id = await upsertWorkspace(db, {
|
|
426
|
+
organizationId: result.organizationId,
|
|
427
|
+
slug: ws.slug,
|
|
428
|
+
name: ws.name,
|
|
429
|
+
description: ws.description ?? null,
|
|
430
|
+
});
|
|
431
|
+
result.workspaces[ws.slug] = id;
|
|
432
|
+
for (const p of wf.defaults.processes) {
|
|
433
|
+
await upsertProcess(db, driver, { workspaceId: id }, p);
|
|
434
|
+
result.processes++;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
defaultWorkspaceId = result.workspaces[wf.defaultWorkspace] ?? null;
|
|
438
|
+
}
|
|
439
|
+
for (const { config, root } of input.projects) {
|
|
440
|
+
const workspaceId = (config.workspace && result.workspaces[config.workspace]) || defaultWorkspaceId;
|
|
441
|
+
const projectId = await upsertProject(db, driver, {
|
|
442
|
+
slug: config.slug,
|
|
443
|
+
name: config.name,
|
|
444
|
+
workspaceId,
|
|
445
|
+
description: config.description ?? null,
|
|
446
|
+
rootPath: root ?? null,
|
|
447
|
+
config: config,
|
|
448
|
+
layers: config.layers,
|
|
449
|
+
browsers: config.browsers,
|
|
450
|
+
tagsPolicy: config.tags,
|
|
451
|
+
screenshotPolicy: config.screenshots,
|
|
452
|
+
});
|
|
453
|
+
result.projects[config.slug] = projectId;
|
|
454
|
+
for (const m of config.modules ?? []) {
|
|
455
|
+
await upsertModule(db, projectId, m);
|
|
456
|
+
result.modules++;
|
|
457
|
+
}
|
|
458
|
+
for (const p of config.processes ?? []) {
|
|
459
|
+
await upsertProcess(db, driver, { projectId }, p);
|
|
460
|
+
result.processes++;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
465
|
+
//# sourceMappingURL=hierarchy.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import type { HealEvent } from '@sdods/contracts/types';
|
|
3
|
+
import type { Driver } from '../driver.js';
|
|
4
|
+
import type { Database } from '../schema.js';
|
|
5
|
+
export declare function insertHealEvent(db: Kysely<Database>, driver: Driver, input: {
|
|
6
|
+
projectId: string;
|
|
7
|
+
runId: string;
|
|
8
|
+
scenarioId?: string | null;
|
|
9
|
+
attemptId?: string | null;
|
|
10
|
+
stepId?: string | null;
|
|
11
|
+
event: HealEvent;
|
|
12
|
+
source?: 'runtime' | 'agent';
|
|
13
|
+
}): Promise<string>;
|
|
14
|
+
export interface LocatorDelta {
|
|
15
|
+
selector: string;
|
|
16
|
+
pageHint?: string | null;
|
|
17
|
+
fails?: number;
|
|
18
|
+
heals?: number;
|
|
19
|
+
uses?: number;
|
|
20
|
+
lastStrategy?: string | null;
|
|
21
|
+
suggestedSelector?: string | null;
|
|
22
|
+
at?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Increment counters for one selector (upsert on project_id + selector). */
|
|
25
|
+
export declare function bumpLocatorStats(db: Kysely<Database>, projectId: string, d: LocatorDelta): Promise<void>;
|
|
26
|
+
export declare function listLocatorStats(db: Kysely<Database>, projectId: string, limit?: number): Promise<{
|
|
27
|
+
id: string;
|
|
28
|
+
selector: string;
|
|
29
|
+
pageHint: string | null;
|
|
30
|
+
failCount: number;
|
|
31
|
+
healCount: number;
|
|
32
|
+
useCount: number;
|
|
33
|
+
lastFailedAt: string | null;
|
|
34
|
+
lastHealedAt: string | null;
|
|
35
|
+
lastStrategy: string | null;
|
|
36
|
+
suggestedSelector: string | null;
|
|
37
|
+
}[]>;
|
|
38
|
+
export declare function listHealEvents(db: Kysely<Database>, opts: {
|
|
39
|
+
projectId?: string;
|
|
40
|
+
runId?: string;
|
|
41
|
+
limit?: number;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
id: string;
|
|
44
|
+
runId: string;
|
|
45
|
+
scenarioId: string | null;
|
|
46
|
+
pageUrl: string | null;
|
|
47
|
+
originalSelector: string;
|
|
48
|
+
context: Record<string, unknown>;
|
|
49
|
+
strategyUsed: string | null;
|
|
50
|
+
healedSelector: string | null;
|
|
51
|
+
candidates: unknown[];
|
|
52
|
+
succeeded: boolean;
|
|
53
|
+
durationMs: number | null;
|
|
54
|
+
createdAt: string | null;
|
|
55
|
+
}[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Recompute flaky stats for one (project, fingerprint, runner_project) over the last `window` runs.
|
|
58
|
+
*/
|
|
59
|
+
export declare function recomputeFlakyStats(db: Kysely<Database>, driver: Driver, input: {
|
|
60
|
+
projectId: string;
|
|
61
|
+
fingerprint: string;
|
|
62
|
+
runnerProject: string;
|
|
63
|
+
window?: number;
|
|
64
|
+
}): Promise<void>;
|
|
65
|
+
export declare function listFlakyStats(db: Kysely<Database>, projectId: string, limit?: number): Promise<{
|
|
66
|
+
id: string;
|
|
67
|
+
fingerprint: string;
|
|
68
|
+
runnerProject: string;
|
|
69
|
+
featureUri: string | null;
|
|
70
|
+
scenarioName: string | null;
|
|
71
|
+
windowSize: number;
|
|
72
|
+
runsCount: number;
|
|
73
|
+
passCount: number;
|
|
74
|
+
failCount: number;
|
|
75
|
+
flakyCount: number;
|
|
76
|
+
flakyRate: number;
|
|
77
|
+
failStreak: number;
|
|
78
|
+
lastStatus: string | null;
|
|
79
|
+
lastRunId: string | null;
|
|
80
|
+
lastFailedAt: string | null;
|
|
81
|
+
quarantined: boolean;
|
|
82
|
+
}[]>;
|
|
83
|
+
export declare function setQuarantine(db: Kysely<Database>, driver: Driver, id: string, quarantined: boolean): Promise<void>;
|
|
84
|
+
//# sourceMappingURL=improvement.d.ts.map
|