@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
package/dist/driver.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Driver = 'sqlite' | 'postgres';
|
|
2
|
+
export interface DriverConfig {
|
|
3
|
+
driver: Driver;
|
|
4
|
+
sqlitePath?: string;
|
|
5
|
+
databaseUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const DEFAULT_SQLITE_PATH = ".sdods/sdods.db";
|
|
8
|
+
/** Resolve the platform database driver from the environment (`DB_DRIVER`, `SQLITE_PATH`, `DATABASE_URL`). */
|
|
9
|
+
export declare function resolveDriverConfig(env?: NodeJS.ProcessEnv): DriverConfig;
|
|
10
|
+
//# sourceMappingURL=driver.d.ts.map
|
package/dist/driver.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const DEFAULT_SQLITE_PATH = '.sdods/sdods.db';
|
|
2
|
+
/** Resolve the platform database driver from the environment (`DB_DRIVER`, `SQLITE_PATH`, `DATABASE_URL`). */
|
|
3
|
+
export function resolveDriverConfig(env = process.env) {
|
|
4
|
+
const driver = (env.DB_DRIVER ?? 'sqlite').toLowerCase();
|
|
5
|
+
if (driver === 'postgres' || driver === 'postgresql' || driver === 'pg') {
|
|
6
|
+
if (!env.DATABASE_URL) {
|
|
7
|
+
throw new Error('DB_DRIVER=postgres requires DATABASE_URL (e.g. postgres://sdods:sdods@localhost:5432/sdods)');
|
|
8
|
+
}
|
|
9
|
+
return { driver: 'postgres', databaseUrl: env.DATABASE_URL };
|
|
10
|
+
}
|
|
11
|
+
if (driver !== 'sqlite') {
|
|
12
|
+
throw new Error(`Unknown DB_DRIVER "${env.DB_DRIVER}". Use sqlite or postgres.`);
|
|
13
|
+
}
|
|
14
|
+
return { driver: 'sqlite', sqlitePath: env.SQLITE_PATH ?? DEFAULT_SQLITE_PATH };
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=driver.js.map
|
package/dist/ids.d.ts
ADDED
package/dist/ids.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from './driver.js';
|
|
2
|
+
export * from './create-db.js';
|
|
3
|
+
export * from './col.js';
|
|
4
|
+
export * from './ids.js';
|
|
5
|
+
export * from './schema.js';
|
|
6
|
+
export * from './migrate.js';
|
|
7
|
+
export * from './repos/index.js';
|
|
8
|
+
export * from './ingest/index.js';
|
|
9
|
+
export * from './switch/index.js';
|
|
10
|
+
export * from './insights.js';
|
|
11
|
+
export * from './test-data.js';
|
|
12
|
+
export * from './scopes.js';
|
|
13
|
+
import { type SdodsDb } from './create-db.js';
|
|
14
|
+
import { type DriverConfig } from './driver.js';
|
|
15
|
+
/** Open + migrate in one call (what `sdods serve` and `sdods run --ingest` do on boot). */
|
|
16
|
+
export declare function openDb(cfg?: DriverConfig): Promise<SdodsDb>;
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export * from './driver.js';
|
|
2
|
+
export * from './create-db.js';
|
|
3
|
+
export * from './col.js';
|
|
4
|
+
export * from './ids.js';
|
|
5
|
+
export * from './schema.js';
|
|
6
|
+
export * from './migrate.js';
|
|
7
|
+
export * from './repos/index.js';
|
|
8
|
+
export * from './ingest/index.js';
|
|
9
|
+
export * from './switch/index.js';
|
|
10
|
+
export * from './insights.js';
|
|
11
|
+
export * from './test-data.js';
|
|
12
|
+
export * from './scopes.js';
|
|
13
|
+
import { createDb } from './create-db.js';
|
|
14
|
+
import { resolveDriverConfig } from './driver.js';
|
|
15
|
+
import { migrateToLatest } from './migrate.js';
|
|
16
|
+
/** Open + migrate in one call (what `sdods serve` and `sdods run --ingest` do on boot). */
|
|
17
|
+
export async function openDb(cfg = resolveDriverConfig()) {
|
|
18
|
+
const adb = createDb(cfg);
|
|
19
|
+
await migrateToLatest(adb);
|
|
20
|
+
return adb;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { parseAttachmentName, type ParsedAttachment } from '@sdods/contracts/names';
|
|
3
|
+
export interface StoredFile {
|
|
4
|
+
relPath: string;
|
|
5
|
+
absPath: string;
|
|
6
|
+
sizeBytes: number;
|
|
7
|
+
sha256: string;
|
|
8
|
+
width: number | null;
|
|
9
|
+
height: number | null;
|
|
10
|
+
}
|
|
11
|
+
export interface AttachmentInput {
|
|
12
|
+
fileName: string;
|
|
13
|
+
mediaType: string;
|
|
14
|
+
body?: string;
|
|
15
|
+
contentEncoding?: 'IDENTITY' | 'BASE64';
|
|
16
|
+
/** absolute or run-relative path when the reporter kept the file external */
|
|
17
|
+
url?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Decode an attachment body to bytes (base64 or identity). */
|
|
20
|
+
export declare function attachmentBytes(a: AttachmentInput): Buffer | null;
|
|
21
|
+
/** Text body as string (JSON attachments are IDENTITY-encoded by playwright-bdd). */
|
|
22
|
+
export declare function attachmentText(a: AttachmentInput): string | null;
|
|
23
|
+
export declare function safeName(name: string): string;
|
|
24
|
+
/** Target file name inside the scenario attempt dir for a parsed SDODS attachment. */
|
|
25
|
+
export declare function targetFileFor(parsed: ParsedAttachment, original: string, mediaType: string): string;
|
|
26
|
+
export declare function extFor(mediaType: string): string;
|
|
27
|
+
/** Write bytes to `<dir>/<file>` unless an identical file already exists there; returns metadata. */
|
|
28
|
+
export declare function storeFile(artifactsRoot: string, dir: string, file: string, bytes: Buffer | null, sourcePath?: string): StoredFile | null;
|
|
29
|
+
/** Read width/height from a PNG IHDR chunk. */
|
|
30
|
+
export declare function pngDimensions(buf: Buffer): {
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
} | null;
|
|
34
|
+
export declare function artifactKindFor(parsed: ParsedAttachment, mediaType: string): {
|
|
35
|
+
kind: string;
|
|
36
|
+
phase: string | null;
|
|
37
|
+
stepIndex: number | null;
|
|
38
|
+
};
|
|
39
|
+
export { parseAttachmentName, basename };
|
|
40
|
+
//# sourceMappingURL=attachments.d.ts.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { basename, dirname, join, relative } from 'node:path';
|
|
4
|
+
import { parseAttachmentName, scenarioFiles } from '@sdods/contracts/names';
|
|
5
|
+
/** Decode an attachment body to bytes (base64 or identity). */
|
|
6
|
+
export function attachmentBytes(a) {
|
|
7
|
+
if (a.url && existsSync(a.url))
|
|
8
|
+
return readFileSync(a.url);
|
|
9
|
+
if (a.body === undefined)
|
|
10
|
+
return null;
|
|
11
|
+
return a.contentEncoding === 'BASE64'
|
|
12
|
+
? Buffer.from(a.body, 'base64')
|
|
13
|
+
: Buffer.from(a.body, 'utf8');
|
|
14
|
+
}
|
|
15
|
+
/** Text body as string (JSON attachments are IDENTITY-encoded by playwright-bdd). */
|
|
16
|
+
export function attachmentText(a) {
|
|
17
|
+
const bytes = attachmentBytes(a);
|
|
18
|
+
return bytes ? bytes.toString('utf8') : null;
|
|
19
|
+
}
|
|
20
|
+
export function safeName(name) {
|
|
21
|
+
return name.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+/, '') || 'attachment';
|
|
22
|
+
}
|
|
23
|
+
/** Target file name inside the scenario attempt dir for a parsed SDODS attachment. */
|
|
24
|
+
export function targetFileFor(parsed, original, mediaType) {
|
|
25
|
+
switch (parsed.kind) {
|
|
26
|
+
case 'shot-scenario':
|
|
27
|
+
return scenarioFiles.scenarioShot(parsed.phase);
|
|
28
|
+
case 'shot-step':
|
|
29
|
+
return scenarioFiles.stepShot(parsed.stepIndex, parsed.phase);
|
|
30
|
+
case 'visual':
|
|
31
|
+
return `visual/${String(parsed.stepIndex).padStart(2, '0')}-${safeName(parsed.name)}${extFor(mediaType)}`;
|
|
32
|
+
case 'api':
|
|
33
|
+
return scenarioFiles.apiJson(parsed.stepIndex, parsed.callIndex, parsed.part);
|
|
34
|
+
case 'perf':
|
|
35
|
+
return scenarioFiles.perfJson(parsed.stepIndex);
|
|
36
|
+
case 'a11y':
|
|
37
|
+
return scenarioFiles.a11yJson(parsed.stepIndex);
|
|
38
|
+
case 'heal':
|
|
39
|
+
return `heal/${String(parsed.stepIndex).padStart(2, '0')}-${parsed.n}.json`;
|
|
40
|
+
case 'cleanup-errors':
|
|
41
|
+
return 'cleanup-errors.json';
|
|
42
|
+
case 'meta':
|
|
43
|
+
return scenarioFiles.meta;
|
|
44
|
+
case 'visual-baseline':
|
|
45
|
+
return `visual/${safeName(parsed.name)}-${parsed.phase}.png`;
|
|
46
|
+
case 'runner-builtin':
|
|
47
|
+
return parsed.name === 'trace'
|
|
48
|
+
? 'trace.zip'
|
|
49
|
+
: parsed.name === 'video'
|
|
50
|
+
? `video${extFor(mediaType) || '.webm'}`
|
|
51
|
+
: 'failure-pw.png';
|
|
52
|
+
default:
|
|
53
|
+
return `attachments/${safeName(original)}${extFor(mediaType)}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function extFor(mediaType) {
|
|
57
|
+
if (!mediaType)
|
|
58
|
+
return '';
|
|
59
|
+
if (mediaType.includes('png'))
|
|
60
|
+
return '.png';
|
|
61
|
+
if (mediaType.includes('jpeg') || mediaType.includes('jpg'))
|
|
62
|
+
return '.jpg';
|
|
63
|
+
if (mediaType.includes('webm'))
|
|
64
|
+
return '.webm';
|
|
65
|
+
if (mediaType.includes('zip'))
|
|
66
|
+
return '.zip';
|
|
67
|
+
if (mediaType.includes('json'))
|
|
68
|
+
return '.json';
|
|
69
|
+
if (mediaType.includes('html'))
|
|
70
|
+
return '.html';
|
|
71
|
+
if (mediaType.startsWith('text/'))
|
|
72
|
+
return '.txt';
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
/** Write bytes to `<dir>/<file>` unless an identical file already exists there; returns metadata. */
|
|
76
|
+
export function storeFile(artifactsRoot, dir, file, bytes, sourcePath) {
|
|
77
|
+
const abs = join(dir, file);
|
|
78
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
79
|
+
if (!existsSync(abs)) {
|
|
80
|
+
if (bytes)
|
|
81
|
+
writeFileSync(abs, bytes);
|
|
82
|
+
else if (sourcePath && existsSync(sourcePath))
|
|
83
|
+
copyFileSync(sourcePath, abs);
|
|
84
|
+
else
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const data = readFileSync(abs);
|
|
88
|
+
const dims = pngDimensions(data);
|
|
89
|
+
return {
|
|
90
|
+
relPath: relative(artifactsRoot, abs).replace(/\\/g, '/'),
|
|
91
|
+
absPath: abs,
|
|
92
|
+
sizeBytes: statSync(abs).size,
|
|
93
|
+
sha256: createHash('sha256').update(data).digest('hex'),
|
|
94
|
+
width: dims?.width ?? null,
|
|
95
|
+
height: dims?.height ?? null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Read width/height from a PNG IHDR chunk. */
|
|
99
|
+
export function pngDimensions(buf) {
|
|
100
|
+
if (buf.length < 24)
|
|
101
|
+
return null;
|
|
102
|
+
if (buf.readUInt32BE(0) !== 0x89504e47)
|
|
103
|
+
return null;
|
|
104
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
105
|
+
}
|
|
106
|
+
export function artifactKindFor(parsed, mediaType) {
|
|
107
|
+
switch (parsed.kind) {
|
|
108
|
+
case 'shot-scenario':
|
|
109
|
+
return {
|
|
110
|
+
kind: 'screenshot',
|
|
111
|
+
phase: parsed.phase === 'start'
|
|
112
|
+
? 'scenario-start'
|
|
113
|
+
: parsed.phase === 'end'
|
|
114
|
+
? 'scenario-end'
|
|
115
|
+
: 'failure',
|
|
116
|
+
stepIndex: null,
|
|
117
|
+
};
|
|
118
|
+
case 'shot-step':
|
|
119
|
+
return { kind: 'screenshot', phase: parsed.phase, stepIndex: parsed.stepIndex };
|
|
120
|
+
case 'visual':
|
|
121
|
+
return { kind: 'visual', phase: 'actual', stepIndex: parsed.stepIndex };
|
|
122
|
+
case 'visual-baseline':
|
|
123
|
+
return { kind: 'visual', phase: parsed.phase, stepIndex: null };
|
|
124
|
+
case 'runner-builtin':
|
|
125
|
+
return {
|
|
126
|
+
kind: parsed.name === 'screenshot' ? 'screenshot' : parsed.name,
|
|
127
|
+
phase: parsed.name === 'screenshot' ? 'failure' : null,
|
|
128
|
+
stepIndex: null,
|
|
129
|
+
};
|
|
130
|
+
case 'api':
|
|
131
|
+
case 'heal':
|
|
132
|
+
case 'perf':
|
|
133
|
+
case 'a11y':
|
|
134
|
+
case 'cleanup-errors':
|
|
135
|
+
case 'meta':
|
|
136
|
+
return {
|
|
137
|
+
kind: 'attachment',
|
|
138
|
+
phase: null,
|
|
139
|
+
stepIndex: 'stepIndex' in parsed ? parsed.stepIndex : null,
|
|
140
|
+
};
|
|
141
|
+
default:
|
|
142
|
+
return {
|
|
143
|
+
kind: mediaType.startsWith('image/')
|
|
144
|
+
? 'screenshot'
|
|
145
|
+
: mediaType.includes('zip')
|
|
146
|
+
? 'trace'
|
|
147
|
+
: 'attachment',
|
|
148
|
+
phase: null,
|
|
149
|
+
stepIndex: null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export { parseAttachmentName, basename };
|
|
154
|
+
//# sourceMappingURL=attachments.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { LocatorCounter } from './finalize.js';
|
|
2
|
+
import { MessageIndex } from './message-index.js';
|
|
3
|
+
import type { Envelope } from './parse-ndjson.js';
|
|
4
|
+
import { type IngestContext } from './types.js';
|
|
5
|
+
export interface CucumberIngestCounts {
|
|
6
|
+
scenarios: number;
|
|
7
|
+
attempts: number;
|
|
8
|
+
steps: number;
|
|
9
|
+
artifacts: number;
|
|
10
|
+
healEvents: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* State machine over cucumber message envelopes. Static messages are indexed; dynamic ones are
|
|
14
|
+
* upserted immediately with natural keys so re-ingest converges.
|
|
15
|
+
*/
|
|
16
|
+
export declare class IngestSession {
|
|
17
|
+
private readonly ctx;
|
|
18
|
+
readonly idx: MessageIndex;
|
|
19
|
+
readonly locatorCounters: Map<string, LocatorCounter>;
|
|
20
|
+
readonly counts: CucumberIngestCounts;
|
|
21
|
+
runStartedAt: string | null;
|
|
22
|
+
runFinishedAt: string | null;
|
|
23
|
+
private readonly attempts;
|
|
24
|
+
private readonly pendingStepStart;
|
|
25
|
+
private readonly seenScenarios;
|
|
26
|
+
private readonly pendingTestCases;
|
|
27
|
+
constructor(ctx: IngestContext);
|
|
28
|
+
consume(env: Envelope): Promise<void>;
|
|
29
|
+
private onAttemptStarted;
|
|
30
|
+
private onStepFinished;
|
|
31
|
+
private readonly pendingPayloads;
|
|
32
|
+
/** Apply a structured (JSON) attachment to its step row / heal table. */
|
|
33
|
+
private applyStructured;
|
|
34
|
+
private onAttachment;
|
|
35
|
+
private onAttemptFinished;
|
|
36
|
+
private stepRowByIndex;
|
|
37
|
+
}
|
|
38
|
+
export declare function layerFromTags(tags: string[]): 'ui' | 'api' | 'hybrid' | null;
|
|
39
|
+
export declare function suiteFromTags(tags: string[]): string | null;
|
|
40
|
+
//# sourceMappingURL=cucumber-ingest.d.ts.map
|