@sdods/db 0.11.2 → 0.12.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.
@@ -0,0 +1,11 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { Col } from '../col.js';
3
+ /**
4
+ * The full selection a run was started with (feature, scenario, workers, headed, HAR mode…), so
5
+ * Rerun can replay it exactly. The existing columns only cover env/tags/layers/browsers, and
6
+ * `command` is an argv joined on spaces that cannot be split back once a scenario name has one.
7
+ * Nullable: runs started before this, and runs from the CLI, fall back to those columns.
8
+ */
9
+ export declare function up(db: Kysely<any>, c: Col): Promise<void>;
10
+ export declare function down(db: Kysely<any>, _c: Col): Promise<void>;
11
+ //# sourceMappingURL=0010_run_params.d.ts.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The full selection a run was started with (feature, scenario, workers, headed, HAR mode…), so
3
+ * Rerun can replay it exactly. The existing columns only cover env/tags/layers/browsers, and
4
+ * `command` is an argv joined on spaces that cannot be split back once a scenario name has one.
5
+ * Nullable: runs started before this, and runs from the CLI, fall back to those columns.
6
+ */
7
+ export async function up(db, c) {
8
+ await db.schema.alterTable('runs').addColumn('params_json', c.json).execute();
9
+ }
10
+ export async function down(db, _c) {
11
+ await db.schema.alterTable('runs').dropColumn('params_json').execute();
12
+ }
13
+ //# sourceMappingURL=0010_run_params.js.map
@@ -0,0 +1,12 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { Col } from '../col.js';
3
+ /**
4
+ * Per-user UI preferences, such as the last run selection for a project (`runForm:<ws>:<project>`).
5
+ *
6
+ * Server-side because browser storage is per origin, and the desktop app's origin is
7
+ * `http://127.0.0.1:<port>`: when its preferred port is taken it binds another, and everything
8
+ * the browser had stored is gone. It also carries a preference from the desktop app to the web UI.
9
+ */
10
+ export declare function up(db: Kysely<any>, c: Col): Promise<void>;
11
+ export declare function down(db: Kysely<any>, _c: Col): Promise<void>;
12
+ //# sourceMappingURL=0011_user_preferences.d.ts.map
@@ -0,0 +1,18 @@
1
+ import { dropTables, fk, index, stamps, table } from './helpers.js';
2
+ /**
3
+ * Per-user UI preferences, such as the last run selection for a project (`runForm:<ws>:<project>`).
4
+ *
5
+ * Server-side because browser storage is per origin, and the desktop app's origin is
6
+ * `http://127.0.0.1:<port>`: when its preferred port is taken it binds another, and everything
7
+ * the browser had stored is gone. It also carries a preference from the desktop app to the web UI.
8
+ */
9
+ export async function up(db, c) {
10
+ await stamps(fk(table(db, 'user_preferences', c), 'user_id', 'users', c)
11
+ .addColumn('key', 'text', (cb) => cb.notNull())
12
+ .addColumn('value_json', c.json, (cb) => cb.notNull()), c).execute();
13
+ await index(db, 'uq_user_preferences_user_key', 'user_preferences', ['user_id', 'key'], true);
14
+ }
15
+ export async function down(db, _c) {
16
+ await dropTables(db, ['user_preferences']);
17
+ }
18
+ //# sourceMappingURL=0011_user_preferences.js.map
@@ -7,6 +7,8 @@ import * as m0006 from './0006_schedules.js';
7
7
  import * as m0007 from './0007_hierarchy.js';
8
8
  import * as m0008 from './0008_runner_naming.js';
9
9
  import * as m0009 from './0009_user_profile.js';
10
+ import * as m0010 from './0010_run_params.js';
11
+ import * as m0011 from './0011_user_preferences.js';
10
12
  /**
11
13
  * Static registry (bundler-friendly, no runtime file discovery). Order matters: Kysely sorts by key.
12
14
  */
@@ -20,5 +22,7 @@ export const MIGRATIONS = {
20
22
  '0007_hierarchy': m0007,
21
23
  '0008_runner_naming': m0008,
22
24
  '0009_user_profile': m0009,
25
+ '0010_run_params': m0010,
26
+ '0011_user_preferences': m0011,
23
27
  };
24
28
  //# sourceMappingURL=index.js.map
@@ -5,4 +5,5 @@ export * from './test-data.js';
5
5
  export * from './auth.js';
6
6
  export * from './platform.js';
7
7
  export * from './hierarchy.js';
8
+ export * from './preferences.js';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,5 @@ export * from './test-data.js';
5
5
  export * from './auth.js';
6
6
  export * from './platform.js';
7
7
  export * from './hierarchy.js';
8
+ export * from './preferences.js';
8
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,12 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { Database } from '../schema.js';
3
+ export interface UserPreference<T = unknown> {
4
+ key: string;
5
+ value: T;
6
+ updatedAt: string | null;
7
+ }
8
+ export declare function getUserPreference<T = unknown>(db: Kysely<Database>, userId: string, key: string): Promise<UserPreference<T> | null>;
9
+ /** Insert or replace one preference; returns the stored row. */
10
+ export declare function setUserPreference<T = unknown>(db: Kysely<Database>, userId: string, key: string, value: T): Promise<UserPreference<T>>;
11
+ export declare function deleteUserPreference(db: Kysely<Database>, userId: string, key: string): Promise<void>;
12
+ //# sourceMappingURL=preferences.d.ts.map
@@ -0,0 +1,56 @@
1
+ import { enc, nowIso, readJson, readTs } from '../col.js';
2
+ import { newId } from '../ids.js';
3
+ export async function getUserPreference(db, userId, key) {
4
+ const row = await db
5
+ .selectFrom('user_preferences')
6
+ .select(['key', 'value_json', 'updated_at'])
7
+ .where('user_id', '=', userId)
8
+ .where('key', '=', key)
9
+ .executeTakeFirst();
10
+ if (!row)
11
+ return null;
12
+ return {
13
+ key: row.key,
14
+ value: readJson(row.value_json),
15
+ updatedAt: readTs(row.updated_at),
16
+ };
17
+ }
18
+ /** Insert or replace one preference; returns the stored row. */
19
+ export async function setUserPreference(db, userId, key, value) {
20
+ const now = nowIso();
21
+ const existing = await db
22
+ .selectFrom('user_preferences')
23
+ .select('id')
24
+ .where('user_id', '=', userId)
25
+ .where('key', '=', key)
26
+ .executeTakeFirst();
27
+ if (existing) {
28
+ await db
29
+ .updateTable('user_preferences')
30
+ .set({ value_json: enc.json(value), updated_at: now })
31
+ .where('id', '=', existing.id)
32
+ .execute();
33
+ }
34
+ else {
35
+ await db
36
+ .insertInto('user_preferences')
37
+ .values({
38
+ id: newId(),
39
+ user_id: userId,
40
+ key,
41
+ value_json: enc.json(value),
42
+ created_at: now,
43
+ updated_at: now,
44
+ })
45
+ .execute();
46
+ }
47
+ return { key, value, updatedAt: now };
48
+ }
49
+ export async function deleteUserPreference(db, userId, key) {
50
+ await db
51
+ .deleteFrom('user_preferences')
52
+ .where('user_id', '=', userId)
53
+ .where('key', '=', key)
54
+ .execute();
55
+ }
56
+ //# sourceMappingURL=preferences.js.map
@@ -31,6 +31,8 @@ export interface RunUpsert {
31
31
  htmlReportRel?: string | null;
32
32
  exitCode?: number | null;
33
33
  errorText?: string | null;
34
+ /** The selection a server-started run was launched with, for Rerun. */
35
+ params?: Record<string, unknown>;
34
36
  }
35
37
  export declare function upsertRun(db: Kysely<Database>, _driver: Driver, r: RunUpsert): Promise<void>;
36
38
  export declare function getRun(db: Kysely<Database>, id: string): Promise<(RunRecord & {
@@ -51,6 +53,16 @@ export declare function mapRun(row: any): RunRecord & {
51
53
  projectId: string;
52
54
  totalsRaw: Record<string, unknown>;
53
55
  };
56
+ /**
57
+ * What a run was started with, for Rerun: the stored selection when the server started it, else
58
+ * the parts the row always records. `null` when the run does not exist.
59
+ */
60
+ export declare function getRunParams(db: Kysely<Database>, id: string): Promise<{
61
+ project: string;
62
+ params: Record<string, unknown>;
63
+ } | null>;
64
+ /** Names of the scenarios that failed in a run, deduplicated (a scenario can run per browser). */
65
+ export declare function getFailedScenarioNames(db: Kysely<Database>, runId: string): Promise<string[]>;
54
66
  export declare function deleteRunChildren(db: Kysely<Database>, runId: string): Promise<void>;
55
67
  export declare function deleteRun(db: Kysely<Database>, runId: string): Promise<void>;
56
68
  /** Scenario tree for a run: scenarios with their attempts (no steps). */
@@ -38,6 +38,7 @@ export async function upsertRun(db, _driver, r) {
38
38
  html_report_rel: r.htmlReportRel,
39
39
  exit_code: r.exitCode,
40
40
  error_text: r.errorText,
41
+ params_json: r.params ? enc.json(r.params) : undefined,
41
42
  });
42
43
  if (existing) {
43
44
  await db
@@ -124,6 +125,53 @@ export function mapRun(row) {
124
125
  errorText: row.error_text ?? undefined,
125
126
  };
126
127
  }
128
+ /**
129
+ * What a run was started with, for Rerun: the stored selection when the server started it, else
130
+ * the parts the row always records. `null` when the run does not exist.
131
+ */
132
+ export async function getRunParams(db, id) {
133
+ const row = await db
134
+ .selectFrom('runs')
135
+ .innerJoin('projects', 'projects.id', 'runs.project_id')
136
+ .select([
137
+ 'projects.slug as project_slug',
138
+ 'runs.params_json',
139
+ 'runs.env_name',
140
+ 'runs.process',
141
+ 'runs.tags_expr',
142
+ 'runs.layers_json',
143
+ 'runs.browsers_json',
144
+ ])
145
+ .where('runs.id', '=', id)
146
+ .executeTakeFirst();
147
+ if (!row)
148
+ return null;
149
+ const stored = readJson(row.params_json);
150
+ if (stored)
151
+ return { project: row.project_slug, params: stored };
152
+ const layers = readJson(row.layers_json) ?? [];
153
+ const browsers = readJson(row.browsers_json) ?? [];
154
+ return {
155
+ project: row.project_slug,
156
+ params: stripUndefined({
157
+ env: row.env_name || undefined,
158
+ process: row.process ?? undefined,
159
+ tags: row.tags_expr ?? undefined,
160
+ layers: layers.length ? layers : undefined,
161
+ browsers: browsers.length ? browsers : undefined,
162
+ }),
163
+ };
164
+ }
165
+ /** Names of the scenarios that failed in a run, deduplicated (a scenario can run per browser). */
166
+ export async function getFailedScenarioNames(db, runId) {
167
+ const rows = await db
168
+ .selectFrom('scenarios')
169
+ .select('scenario_name')
170
+ .where('run_id', '=', runId)
171
+ .where('status', 'in', ['failed', 'timedOut', 'interrupted'])
172
+ .execute();
173
+ return [...new Set(rows.map((r) => r.scenario_name))];
174
+ }
127
175
  export async function deleteRunChildren(db, runId) {
128
176
  // FK cascades handle steps/attempts/artifacts/heal_events; delete explicitly for engines without FK enforcement.
129
177
  await db.deleteFrom('heal_events').where('run_id', '=', runId).execute();
package/dist/schema.d.ts CHANGED
@@ -133,6 +133,8 @@ export interface RunsTable {
133
133
  html_report_rel: string | null;
134
134
  exit_code: number | null;
135
135
  error_text: string | null;
136
+ /** The StartRunInput a server-started run was launched with; null for CLI runs and older rows. */
137
+ params_json: Json<Record<string, unknown>> | null;
136
138
  created_at: Ts;
137
139
  updated_at: Ts;
138
140
  }
@@ -475,6 +477,14 @@ export interface AuditLogTable {
475
477
  ip: string | null;
476
478
  created_at: Ts;
477
479
  }
480
+ export interface UserPreferencesTable {
481
+ id: string;
482
+ user_id: string;
483
+ key: string;
484
+ value_json: Json<unknown>;
485
+ created_at: Ts;
486
+ updated_at: Ts;
487
+ }
478
488
  export interface Database {
479
489
  organizations: OrganizationsTable;
480
490
  workspaces: WorkspacesTable;
@@ -507,6 +517,7 @@ export interface Database {
507
517
  schedules: SchedulesTable;
508
518
  schedule_runs: ScheduleRunsTable;
509
519
  audit_log: AuditLogTable;
520
+ user_preferences: UserPreferencesTable;
510
521
  }
511
522
  /** Platform tables in foreign-key order (parents first). Used by export/import/switch/prune. */
512
523
  export declare const TABLES_IN_FK_ORDER: Array<keyof Database>;
package/dist/schema.js CHANGED
@@ -4,6 +4,7 @@ export const TABLES_IN_FK_ORDER = [
4
4
  'users',
5
5
  'sessions',
6
6
  'api_tokens',
7
+ 'user_preferences',
7
8
  'organizations',
8
9
  'workspaces',
9
10
  'org_members',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdods/db",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
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>",
@@ -17,13 +17,14 @@
17
17
  "access": "public"
18
18
  },
19
19
  "files": [
20
- "dist"
20
+ "dist",
21
+ "NOTICE"
21
22
  ],
22
23
  "engines": {
23
24
  "node": ">=22"
24
25
  },
25
26
  "dependencies": {
26
- "@sdods/contracts": "0.11.2",
27
+ "@sdods/contracts": "0.12.0",
27
28
  "better-sqlite3": "^13.0.3",
28
29
  "kysely": "^0.29.5",
29
30
  "pg": "^8.23.0",