@sdods/db 0.2.0 → 0.2.1
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/.tsbuildinfo +1 -1
- package/dist/driver.d.ts +9 -0
- package/dist/driver.js +35 -1
- package/dist/ingest/cucumber-ingest.js +0 -12
- package/dist/repos/auth.d.ts +2 -1
- package/dist/repos/auth.js +4 -2
- package/dist/repos/projects.d.ts +2 -2
- package/package.json +2 -2
- package/dist/ingest/pw-json-ingest.d.ts +0 -14
- package/dist/ingest/pw-json-ingest.js +0 -259
package/dist/driver.d.ts
CHANGED
|
@@ -5,6 +5,15 @@ export interface DriverConfig {
|
|
|
5
5
|
databaseUrl?: string;
|
|
6
6
|
}
|
|
7
7
|
export declare const DEFAULT_SQLITE_PATH = ".sdods/sdods.db";
|
|
8
|
+
/**
|
|
9
|
+
* Nearest ancestor of `start` that looks like an SDODS workspace, or null.
|
|
10
|
+
*
|
|
11
|
+
* The default sqlite path is relative, and resolving it against the current directory meant every
|
|
12
|
+
* command opened a *different* database depending on where it was run from. `sdods serve` started
|
|
13
|
+
* inside a run's artifacts folder would silently create an empty database there and then reject
|
|
14
|
+
* every login, because the users live in the workspace database one or four levels up.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findWorkspaceRoot(start?: string): string | null;
|
|
8
17
|
/** Resolve the platform database driver from the environment (`DB_DRIVER`, `SQLITE_PATH`, `DATABASE_URL`). */
|
|
9
18
|
export declare function resolveDriverConfig(env?: NodeJS.ProcessEnv): DriverConfig;
|
|
10
19
|
//# sourceMappingURL=driver.d.ts.map
|
package/dist/driver.js
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
1
3
|
export const DEFAULT_SQLITE_PATH = '.sdods/sdods.db';
|
|
4
|
+
// Mirrors the markers ProjectRegistry.findRepoRoot looks for. Duplicated rather than imported
|
|
5
|
+
// because @sdods/db must not depend on @sdods/core (core already depends on db).
|
|
6
|
+
const WORKSPACE_MARKERS = ['sdods.workspace.yaml', 'sdods.config.ts', 'sdods.config.json'];
|
|
7
|
+
/**
|
|
8
|
+
* Nearest ancestor of `start` that looks like an SDODS workspace, or null.
|
|
9
|
+
*
|
|
10
|
+
* The default sqlite path is relative, and resolving it against the current directory meant every
|
|
11
|
+
* command opened a *different* database depending on where it was run from. `sdods serve` started
|
|
12
|
+
* inside a run's artifacts folder would silently create an empty database there and then reject
|
|
13
|
+
* every login, because the users live in the workspace database one or four levels up.
|
|
14
|
+
*/
|
|
15
|
+
export function findWorkspaceRoot(start = process.cwd()) {
|
|
16
|
+
let dir = resolve(start);
|
|
17
|
+
for (let i = 0; i < 12; i++) {
|
|
18
|
+
if (WORKSPACE_MARKERS.some((m) => existsSync(join(dir, m))))
|
|
19
|
+
return dir;
|
|
20
|
+
const parent = dirname(dir);
|
|
21
|
+
if (parent === dir)
|
|
22
|
+
break;
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
2
27
|
/** Resolve the platform database driver from the environment (`DB_DRIVER`, `SQLITE_PATH`, `DATABASE_URL`). */
|
|
3
28
|
export function resolveDriverConfig(env = process.env) {
|
|
4
29
|
const driver = (env.DB_DRIVER ?? 'sqlite').toLowerCase();
|
|
@@ -11,6 +36,15 @@ export function resolveDriverConfig(env = process.env) {
|
|
|
11
36
|
if (driver !== 'sqlite') {
|
|
12
37
|
throw new Error(`Unknown DB_DRIVER "${env.DB_DRIVER}". Use sqlite or postgres.`);
|
|
13
38
|
}
|
|
14
|
-
|
|
39
|
+
// An explicit SQLITE_PATH is taken at face value: the caller said where they want it. Only the
|
|
40
|
+
// default is anchored to the workspace, so `sdods serve` and `sdods users` agree on one database
|
|
41
|
+
// no matter which subdirectory they run from.
|
|
42
|
+
if (env.SQLITE_PATH)
|
|
43
|
+
return { driver: 'sqlite', sqlitePath: env.SQLITE_PATH };
|
|
44
|
+
const root = findWorkspaceRoot();
|
|
45
|
+
return {
|
|
46
|
+
driver: 'sqlite',
|
|
47
|
+
sqlitePath: root ? join(root, DEFAULT_SQLITE_PATH) : DEFAULT_SQLITE_PATH,
|
|
48
|
+
};
|
|
15
49
|
}
|
|
16
50
|
//# sourceMappingURL=driver.js.map
|
|
@@ -375,18 +375,6 @@ export class IngestSession {
|
|
|
375
375
|
const meta = json;
|
|
376
376
|
if (meta.dir)
|
|
377
377
|
a.dir = meta.dir;
|
|
378
|
-
// The runtime resolved the module from the project config (moduleForFeature), which is
|
|
379
|
-
// authoritative; the scenario row was inserted earlier with moduleFromUri(), a directory
|
|
380
|
-
// guess that is wrong whenever a module's `path` differs from its `name` (demo-shop's
|
|
381
|
-
// `posts-api` lives in features/api/, so the guess yields "api" and never joins to
|
|
382
|
-
// modules.name). Correct it now that the meta attachment has arrived.
|
|
383
|
-
if (meta.module) {
|
|
384
|
-
await this.ctx.adb.db
|
|
385
|
-
.updateTable('scenarios')
|
|
386
|
-
.set({ module: meta.module })
|
|
387
|
-
.where('id', '=', a.scenarioId)
|
|
388
|
-
.execute();
|
|
389
|
-
}
|
|
390
378
|
}
|
|
391
379
|
else if (json) {
|
|
392
380
|
const stepIndex = parsed.stepIndex;
|
package/dist/repos/auth.d.ts
CHANGED
|
@@ -105,5 +105,6 @@ export declare function listApiTokens(db: Kysely<Database>, userId?: string): Pr
|
|
|
105
105
|
revokedAt: string | null;
|
|
106
106
|
createdAt: string | null;
|
|
107
107
|
}[]>;
|
|
108
|
-
|
|
108
|
+
/** True when a live token was revoked; false when the id is unknown or already revoked. */
|
|
109
|
+
export declare function revokeApiToken(db: Kysely<Database>, id: string): Promise<boolean>;
|
|
109
110
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/repos/auth.js
CHANGED
|
@@ -247,12 +247,14 @@ export async function listApiTokens(db, userId) {
|
|
|
247
247
|
createdAt: readTs(r.created_at),
|
|
248
248
|
}));
|
|
249
249
|
}
|
|
250
|
+
/** True when a live token was revoked; false when the id is unknown or already revoked. */
|
|
250
251
|
export async function revokeApiToken(db, id) {
|
|
251
|
-
await db
|
|
252
|
+
const res = await db
|
|
252
253
|
.updateTable('api_tokens')
|
|
253
254
|
.set({ revoked_at: nowIso() })
|
|
254
255
|
.where('id', '=', id)
|
|
255
256
|
.where('revoked_at', 'is', null)
|
|
256
|
-
.
|
|
257
|
+
.executeTakeFirst();
|
|
258
|
+
return Number(res?.numUpdatedRows ?? 0) > 0;
|
|
257
259
|
}
|
|
258
260
|
//# sourceMappingURL=auth.js.map
|
package/dist/repos/projects.d.ts
CHANGED
|
@@ -74,10 +74,10 @@ export declare function upsertEnvironment(db: Kysely<Database>, driver: Driver,
|
|
|
74
74
|
}): Promise<string>;
|
|
75
75
|
export declare function getEnvironment(db: Kysely<Database>, projectId: string, name: string): Promise<{
|
|
76
76
|
id: string;
|
|
77
|
-
name: string;
|
|
78
|
-
config_json: Record<string, unknown>;
|
|
79
77
|
created_at: string;
|
|
80
78
|
updated_at: string;
|
|
79
|
+
name: string;
|
|
80
|
+
config_json: Record<string, unknown>;
|
|
81
81
|
project_id: string;
|
|
82
82
|
base_url: string | null;
|
|
83
83
|
api_base_url: string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/db",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "SDODS platform database: one schema on SQLite or Postgres, migrations, result ingest and insights.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SDODS <admin@sdods.com>",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"node": ">=22"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@sdods/contracts": "0.2.
|
|
26
|
+
"@sdods/contracts": "0.2.1",
|
|
27
27
|
"better-sqlite3": "^13.0.3",
|
|
28
28
|
"kysely": "^0.29.5",
|
|
29
29
|
"pg": "^8.23.0",
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { LocatorCounter } from './finalize.js';
|
|
2
|
-
import { type IngestContext } from './types.js';
|
|
3
|
-
export interface PwJsonCounts {
|
|
4
|
-
scenarios: number;
|
|
5
|
-
attempts: number;
|
|
6
|
-
steps: number;
|
|
7
|
-
artifacts: number;
|
|
8
|
-
}
|
|
9
|
-
/** Ingest a Playwright `json` reporter file (recorded layer / plain specs) into the same tables. */
|
|
10
|
-
export declare function ingestPwJson(ctx: IngestContext, file: string, locatorCounters: Map<string, LocatorCounter>): Promise<PwJsonCounts & {
|
|
11
|
-
startedAt: string | null;
|
|
12
|
-
finishedAt: string | null;
|
|
13
|
-
}>;
|
|
14
|
-
//# sourceMappingURL=pw-json-ingest.d.ts.map
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { basename, dirname, isAbsolute, join, relative } from 'node:path';
|
|
3
|
-
import { fingerprint as makeFingerprint, newId, parseRunnerProjectName } from '@sdods/contracts/ids';
|
|
4
|
-
import { enc, nowIso } from '../col.js';
|
|
5
|
-
import { artifactKindFor, parseAttachmentName, storeFile, targetFileFor } from './attachments.js';
|
|
6
|
-
import { moduleFromUri, pwStatus, selectorFromError } from './types.js';
|
|
7
|
-
/** Ingest a Playwright `json` reporter file (recorded layer / plain specs) into the same tables. */
|
|
8
|
-
export async function ingestPwJson(ctx, file, locatorCounters) {
|
|
9
|
-
const report = JSON.parse(readFileSync(file, 'utf8'));
|
|
10
|
-
const counts = { scenarios: 0, attempts: 0, steps: 0, artifacts: 0 };
|
|
11
|
-
let startedAt = null;
|
|
12
|
-
let finishedAt = null;
|
|
13
|
-
const { db, driver } = ctx.adb;
|
|
14
|
-
const rootDir = report.config?.rootDir ?? dirname(file);
|
|
15
|
-
const walk = async (suite) => {
|
|
16
|
-
for (const s of suite.suites ?? [])
|
|
17
|
-
await walk(s);
|
|
18
|
-
for (const spec of suite.specs ?? []) {
|
|
19
|
-
for (const test of spec.tests ?? []) {
|
|
20
|
-
const pwProject = test.projectName ?? test.projectId ?? 'recorded';
|
|
21
|
-
const parts = parseRunnerProjectName(pwProject);
|
|
22
|
-
const slug = parts?.project ?? ctx.projectSlug;
|
|
23
|
-
const layer = parts?.layer ?? 'recorded';
|
|
24
|
-
const browser = parts?.browser ?? null;
|
|
25
|
-
const specFile = String(spec.file ?? suite.file ?? 'unknown.spec.ts').replace(/\\/g, '/');
|
|
26
|
-
const title = spec.title ?? test.title ?? 'untitled';
|
|
27
|
-
const fp = makeFingerprint({
|
|
28
|
-
project: slug,
|
|
29
|
-
featureUri: specFile,
|
|
30
|
-
scenarioName: title,
|
|
31
|
-
exampleIndex: null,
|
|
32
|
-
layer,
|
|
33
|
-
});
|
|
34
|
-
const naturalKey = `${pwProject}:${test.id ?? `${specFile}:${spec.line ?? 0}`}`;
|
|
35
|
-
const tags = (spec.tags ?? test.tags ?? []).map((t) => t.startsWith('@') ? t : `@${t}`);
|
|
36
|
-
let scenarioId;
|
|
37
|
-
const existing = await db
|
|
38
|
-
.selectFrom('scenarios')
|
|
39
|
-
.select(['id'])
|
|
40
|
-
.where('run_id', '=', ctx.runId)
|
|
41
|
-
.where('natural_key', '=', naturalKey)
|
|
42
|
-
.executeTakeFirst();
|
|
43
|
-
if (existing)
|
|
44
|
-
scenarioId = existing.id;
|
|
45
|
-
else {
|
|
46
|
-
scenarioId = newId();
|
|
47
|
-
await db
|
|
48
|
-
.insertInto('scenarios')
|
|
49
|
-
.values({
|
|
50
|
-
id: scenarioId,
|
|
51
|
-
run_id: ctx.runId,
|
|
52
|
-
project_id: ctx.projectId,
|
|
53
|
-
natural_key: naturalKey,
|
|
54
|
-
fingerprint: fp,
|
|
55
|
-
source: 'pw-json',
|
|
56
|
-
feature_uri: specFile,
|
|
57
|
-
feature_name: suite.title ?? basename(specFile),
|
|
58
|
-
scenario_name: title,
|
|
59
|
-
module: moduleFromUri(specFile),
|
|
60
|
-
examples_row: null,
|
|
61
|
-
pw_project: pwProject,
|
|
62
|
-
layer,
|
|
63
|
-
browser,
|
|
64
|
-
suite_tag: tags.find((t) => ['@smoke', '@regression', '@sanity'].includes(t)) ?? null,
|
|
65
|
-
tags_json: enc.json(tags),
|
|
66
|
-
jira_keys_json: enc.json(tags.filter((t) => t.startsWith('@jira:')).map((t) => t.slice(6))),
|
|
67
|
-
status: 'unknown',
|
|
68
|
-
attempts_count: 0,
|
|
69
|
-
flaky: enc.bool(driver, false),
|
|
70
|
-
duration_ms: null,
|
|
71
|
-
error_message: null,
|
|
72
|
-
error_stack: null,
|
|
73
|
-
started_at: null,
|
|
74
|
-
finished_at: null,
|
|
75
|
-
created_at: nowIso(),
|
|
76
|
-
updated_at: nowIso(),
|
|
77
|
-
})
|
|
78
|
-
.execute();
|
|
79
|
-
counts.scenarios++;
|
|
80
|
-
}
|
|
81
|
-
for (const result of test.results ?? []) {
|
|
82
|
-
const retry = Number(result.retry ?? 0);
|
|
83
|
-
const tcsId = `${test.id ?? naturalKey}#${retry}`;
|
|
84
|
-
const status = pwStatus(result.status);
|
|
85
|
-
const started = result.startTime ? new Date(result.startTime).toISOString() : null;
|
|
86
|
-
const finished = started && typeof result.duration === 'number'
|
|
87
|
-
? new Date(Date.parse(started) + result.duration).toISOString()
|
|
88
|
-
: null;
|
|
89
|
-
startedAt =
|
|
90
|
-
!startedAt || (started && started < startedAt) ? (started ?? startedAt) : startedAt;
|
|
91
|
-
finishedAt =
|
|
92
|
-
!finishedAt || (finished && finished > finishedAt)
|
|
93
|
-
? (finished ?? finishedAt)
|
|
94
|
-
: finishedAt;
|
|
95
|
-
const errorMessage = result.error?.message ?? result.errors?.[0]?.message ?? null;
|
|
96
|
-
const errorStack = result.error?.stack ?? result.errors?.[0]?.stack ?? null;
|
|
97
|
-
if (status === 'failed' || status === 'timedOut') {
|
|
98
|
-
const selector = selectorFromError(errorStack ?? errorMessage);
|
|
99
|
-
if (selector) {
|
|
100
|
-
const c = locatorCounters.get(selector) ?? { fails: 0, heals: 0, uses: 0 };
|
|
101
|
-
c.fails++;
|
|
102
|
-
c.uses++;
|
|
103
|
-
locatorCounters.set(selector, c);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
let attemptId;
|
|
107
|
-
const ex = await db
|
|
108
|
-
.selectFrom('scenario_attempts')
|
|
109
|
-
.select(['id'])
|
|
110
|
-
.where('run_id', '=', ctx.runId)
|
|
111
|
-
.where('test_case_started_id', '=', tcsId)
|
|
112
|
-
.executeTakeFirst();
|
|
113
|
-
const values = {
|
|
114
|
-
attempt: retry,
|
|
115
|
-
status,
|
|
116
|
-
duration_ms: typeof result.duration === 'number' ? result.duration : null,
|
|
117
|
-
error_message: errorMessage ? errorMessage.split('\n')[0].slice(0, 2000) : null,
|
|
118
|
-
error_stack: errorStack,
|
|
119
|
-
will_be_retried: enc.bool(driver, (status === 'failed' || status === 'timedOut') && retry < Number(test.retries ?? 0)),
|
|
120
|
-
worker_index: typeof result.workerIndex === 'number' ? result.workerIndex : null,
|
|
121
|
-
started_at: started,
|
|
122
|
-
finished_at: finished,
|
|
123
|
-
};
|
|
124
|
-
if (ex) {
|
|
125
|
-
attemptId = ex.id;
|
|
126
|
-
await db
|
|
127
|
-
.updateTable('scenario_attempts')
|
|
128
|
-
.set(values)
|
|
129
|
-
.where('id', '=', attemptId)
|
|
130
|
-
.execute();
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
attemptId = newId();
|
|
134
|
-
await db
|
|
135
|
-
.insertInto('scenario_attempts')
|
|
136
|
-
.values({
|
|
137
|
-
id: attemptId,
|
|
138
|
-
scenario_id: scenarioId,
|
|
139
|
-
run_id: ctx.runId,
|
|
140
|
-
test_case_started_id: tcsId,
|
|
141
|
-
created_at: nowIso(),
|
|
142
|
-
...values,
|
|
143
|
-
})
|
|
144
|
-
.execute();
|
|
145
|
-
counts.attempts++;
|
|
146
|
-
}
|
|
147
|
-
// steps (category test.step, top level)
|
|
148
|
-
const steps = (result.steps ?? []).filter((s) => !s.category || s.category === 'test.step');
|
|
149
|
-
let idx = 0;
|
|
150
|
-
for (const st of steps) {
|
|
151
|
-
const testStepId = `${tcsId}-step-${idx}`;
|
|
152
|
-
const stepStatus = st.error ? 'failed' : 'passed';
|
|
153
|
-
const sv = {
|
|
154
|
-
step_index: idx,
|
|
155
|
-
kind: 'step',
|
|
156
|
-
hook_type: null,
|
|
157
|
-
keyword: null,
|
|
158
|
-
text: String(st.title ?? ''),
|
|
159
|
-
argument_json: null,
|
|
160
|
-
status: stepStatus,
|
|
161
|
-
duration_ms: typeof st.duration === 'number' ? st.duration : null,
|
|
162
|
-
error_message: st.error?.message
|
|
163
|
-
? String(st.error.message).split('\n')[0].slice(0, 2000)
|
|
164
|
-
: null,
|
|
165
|
-
error_stack: st.error?.stack ?? null,
|
|
166
|
-
definition_location: st.location ? `${st.location.file}:${st.location.line}` : null,
|
|
167
|
-
pickle_step_id: null,
|
|
168
|
-
layer_hint: 'ui',
|
|
169
|
-
started_at: null,
|
|
170
|
-
finished_at: null,
|
|
171
|
-
};
|
|
172
|
-
const es = await db
|
|
173
|
-
.selectFrom('steps')
|
|
174
|
-
.select(['id'])
|
|
175
|
-
.where('attempt_id', '=', attemptId)
|
|
176
|
-
.where('test_step_id', '=', testStepId)
|
|
177
|
-
.executeTakeFirst();
|
|
178
|
-
if (es)
|
|
179
|
-
await db.updateTable('steps').set(sv).where('id', '=', es.id).execute();
|
|
180
|
-
else {
|
|
181
|
-
await db
|
|
182
|
-
.insertInto('steps')
|
|
183
|
-
.values({
|
|
184
|
-
id: newId(),
|
|
185
|
-
attempt_id: attemptId,
|
|
186
|
-
scenario_id: scenarioId,
|
|
187
|
-
run_id: ctx.runId,
|
|
188
|
-
test_step_id: testStepId,
|
|
189
|
-
api_snapshot_json: null,
|
|
190
|
-
perf_json: null,
|
|
191
|
-
created_at: nowIso(),
|
|
192
|
-
...sv,
|
|
193
|
-
})
|
|
194
|
-
.execute();
|
|
195
|
-
counts.steps++;
|
|
196
|
-
}
|
|
197
|
-
idx++;
|
|
198
|
-
}
|
|
199
|
-
// attachments
|
|
200
|
-
const dir = join(ctx.runDir, slug, fp, `r${retry}`);
|
|
201
|
-
for (const att of result.attachments ?? []) {
|
|
202
|
-
const name = att.name ?? 'attachment';
|
|
203
|
-
const mediaType = att.contentType ?? 'application/octet-stream';
|
|
204
|
-
const parsed = parseAttachmentName(name);
|
|
205
|
-
const filePath = att.path
|
|
206
|
-
? isAbsolute(att.path)
|
|
207
|
-
? att.path
|
|
208
|
-
: join(rootDir, att.path)
|
|
209
|
-
: undefined;
|
|
210
|
-
const bytes = att.body ? Buffer.from(att.body, 'base64') : null;
|
|
211
|
-
if (!bytes && !(filePath && existsSync(filePath)))
|
|
212
|
-
continue;
|
|
213
|
-
const stored = storeFile(ctx.artifactsRoot, dir, targetFileFor(parsed, name, mediaType), bytes, filePath);
|
|
214
|
-
if (!stored)
|
|
215
|
-
continue;
|
|
216
|
-
const { kind, phase, stepIndex } = artifactKindFor(parsed, mediaType);
|
|
217
|
-
const ea = await db
|
|
218
|
-
.selectFrom('artifacts')
|
|
219
|
-
.select(['id'])
|
|
220
|
-
.where('rel_path', '=', stored.relPath)
|
|
221
|
-
.executeTakeFirst();
|
|
222
|
-
const av = {
|
|
223
|
-
run_id: ctx.runId,
|
|
224
|
-
scenario_id: scenarioId,
|
|
225
|
-
attempt_id: attemptId,
|
|
226
|
-
step_id: null,
|
|
227
|
-
step_index: stepIndex,
|
|
228
|
-
kind,
|
|
229
|
-
phase,
|
|
230
|
-
media_type: mediaType,
|
|
231
|
-
file_name: name,
|
|
232
|
-
size_bytes: stored.sizeBytes,
|
|
233
|
-
sha256: stored.sha256,
|
|
234
|
-
width: stored.width,
|
|
235
|
-
height: stored.height,
|
|
236
|
-
meta_json: enc.json({
|
|
237
|
-
original: name,
|
|
238
|
-
source: filePath ? relative(rootDir, filePath) : 'inline',
|
|
239
|
-
}),
|
|
240
|
-
};
|
|
241
|
-
if (ea)
|
|
242
|
-
await db.updateTable('artifacts').set(av).where('id', '=', ea.id).execute();
|
|
243
|
-
else {
|
|
244
|
-
await db
|
|
245
|
-
.insertInto('artifacts')
|
|
246
|
-
.values({ id: newId(), rel_path: stored.relPath, created_at: nowIso(), ...av })
|
|
247
|
-
.execute();
|
|
248
|
-
counts.artifacts++;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
for (const s of report.suites ?? [])
|
|
256
|
-
await walk(s);
|
|
257
|
-
return { ...counts, startedAt, finishedAt };
|
|
258
|
-
}
|
|
259
|
-
//# sourceMappingURL=pw-json-ingest.js.map
|