@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.
Files changed (87) hide show
  1. package/LICENSE +17 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/col.d.ts +32 -0
  4. package/dist/col.js +58 -0
  5. package/dist/create-db.d.ts +14 -0
  6. package/dist/create-db.js +58 -0
  7. package/dist/driver.d.ts +10 -0
  8. package/dist/driver.js +16 -0
  9. package/dist/ids.d.ts +2 -0
  10. package/dist/ids.js +2 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.js +22 -0
  13. package/dist/ingest/attachments.d.ts +40 -0
  14. package/dist/ingest/attachments.js +154 -0
  15. package/dist/ingest/cucumber-ingest.d.ts +40 -0
  16. package/dist/ingest/cucumber-ingest.js +543 -0
  17. package/dist/ingest/finalize.d.ts +28 -0
  18. package/dist/ingest/finalize.js +132 -0
  19. package/dist/ingest/index.d.ts +26 -0
  20. package/dist/ingest/index.js +193 -0
  21. package/dist/ingest/message-index.d.ts +64 -0
  22. package/dist/ingest/message-index.js +146 -0
  23. package/dist/ingest/parse-ndjson.d.ts +10 -0
  24. package/dist/ingest/parse-ndjson.js +35 -0
  25. package/dist/ingest/pw-json-ingest.d.ts +14 -0
  26. package/dist/ingest/pw-json-ingest.js +259 -0
  27. package/dist/ingest/runner-json-ingest.d.ts +14 -0
  28. package/dist/ingest/runner-json-ingest.js +259 -0
  29. package/dist/ingest/types.d.ts +62 -0
  30. package/dist/ingest/types.js +89 -0
  31. package/dist/insights.d.ts +62 -0
  32. package/dist/insights.js +184 -0
  33. package/dist/migrate.d.ts +30 -0
  34. package/dist/migrate.js +114 -0
  35. package/dist/migrations/0001_platform_init.d.ts +5 -0
  36. package/dist/migrations/0001_platform_init.js +48 -0
  37. package/dist/migrations/0002_results.d.ts +5 -0
  38. package/dist/migrations/0002_results.js +124 -0
  39. package/dist/migrations/0003_improvement.d.ts +5 -0
  40. package/dist/migrations/0003_improvement.js +56 -0
  41. package/dist/migrations/0004_test_data.d.ts +5 -0
  42. package/dist/migrations/0004_test_data.js +48 -0
  43. package/dist/migrations/0005_integrations_agents_audit.d.ts +5 -0
  44. package/dist/migrations/0005_integrations_agents_audit.js +100 -0
  45. package/dist/migrations/0006_schedules.d.ts +5 -0
  46. package/dist/migrations/0006_schedules.js +34 -0
  47. package/dist/migrations/0007_hierarchy.d.ts +5 -0
  48. package/dist/migrations/0007_hierarchy.js +76 -0
  49. package/dist/migrations/0008_runner_naming.d.ts +14 -0
  50. package/dist/migrations/0008_runner_naming.js +35 -0
  51. package/dist/migrations/helpers.d.ts +15 -0
  52. package/dist/migrations/helpers.js +30 -0
  53. package/dist/migrations/index.d.ts +6 -0
  54. package/dist/migrations/index.js +22 -0
  55. package/dist/repos/auth.d.ts +109 -0
  56. package/dist/repos/auth.js +258 -0
  57. package/dist/repos/hierarchy.d.ts +151 -0
  58. package/dist/repos/hierarchy.js +465 -0
  59. package/dist/repos/improvement.d.ts +84 -0
  60. package/dist/repos/improvement.js +242 -0
  61. package/dist/repos/index.d.ts +8 -0
  62. package/dist/repos/index.js +8 -0
  63. package/dist/repos/platform.d.ts +250 -0
  64. package/dist/repos/platform.js +429 -0
  65. package/dist/repos/projects.d.ts +87 -0
  66. package/dist/repos/projects.js +118 -0
  67. package/dist/repos/runs.d.ts +306 -0
  68. package/dist/repos/runs.js +321 -0
  69. package/dist/repos/test-data.d.ts +84 -0
  70. package/dist/repos/test-data.js +291 -0
  71. package/dist/schema.d.ts +514 -0
  72. package/dist/schema.js +37 -0
  73. package/dist/scopes.d.ts +2 -0
  74. package/dist/scopes.js +2 -0
  75. package/dist/switch/export.d.ts +15 -0
  76. package/dist/switch/export.js +67 -0
  77. package/dist/switch/import.d.ts +16 -0
  78. package/dist/switch/import.js +106 -0
  79. package/dist/switch/index.d.ts +5 -0
  80. package/dist/switch/index.js +5 -0
  81. package/dist/switch/prune.d.ts +19 -0
  82. package/dist/switch/prune.js +50 -0
  83. package/dist/switch/switch.d.ts +34 -0
  84. package/dist/switch/switch.js +133 -0
  85. package/dist/test-data.d.ts +22 -0
  86. package/dist/test-data.js +79 -0
  87. package/package.json +50 -0
@@ -0,0 +1,258 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { API_TOKEN_PREFIX, ROLES, isScope, scopesForRole, } from '@sdods/contracts/scopes';
3
+ import { enc, nowIso, readBool, readJson, readTs } from '../col.js';
4
+ import { newId } from '../ids.js';
5
+ /** Seed the three built-in roles (idempotent). */
6
+ export async function ensureRoles(db) {
7
+ const out = {};
8
+ for (const name of ROLES) {
9
+ const existing = await db
10
+ .selectFrom('roles')
11
+ .select(['id'])
12
+ .where('name', '=', name)
13
+ .executeTakeFirst();
14
+ if (existing) {
15
+ out[name] = existing.id;
16
+ continue;
17
+ }
18
+ const id = newId();
19
+ await db
20
+ .insertInto('roles')
21
+ .values({ id, name, permissions_json: enc.json(scopesForRole(name)), created_at: nowIso() })
22
+ .execute();
23
+ out[name] = id;
24
+ }
25
+ return out;
26
+ }
27
+ export async function createUser(db, driver, input) {
28
+ const roles = await ensureRoles(db);
29
+ const id = newId();
30
+ await db
31
+ .insertInto('users')
32
+ .values({
33
+ id,
34
+ username: input.username,
35
+ email: input.email ?? null,
36
+ password_hash: input.passwordHash,
37
+ role_id: roles[input.role],
38
+ active: enc.bool(driver, true),
39
+ auth_provider: input.authProvider ?? 'local',
40
+ external_id: input.externalId ?? null,
41
+ last_login_at: null,
42
+ created_at: nowIso(),
43
+ updated_at: nowIso(),
44
+ })
45
+ .execute();
46
+ return id;
47
+ }
48
+ export async function getUserByUsername(db, username) {
49
+ const row = await db
50
+ .selectFrom('users')
51
+ .innerJoin('roles', 'roles.id', 'users.role_id')
52
+ .selectAll('users')
53
+ .select('roles.name as role')
54
+ .where('users.username', '=', username)
55
+ .executeTakeFirst();
56
+ return row ? mapUser(row) : null;
57
+ }
58
+ export async function getUserById(db, id) {
59
+ const row = await db
60
+ .selectFrom('users')
61
+ .innerJoin('roles', 'roles.id', 'users.role_id')
62
+ .selectAll('users')
63
+ .select('roles.name as role')
64
+ .where('users.id', '=', id)
65
+ .executeTakeFirst();
66
+ return row ? mapUser(row) : null;
67
+ }
68
+ export async function listUsers(db) {
69
+ const rows = await db
70
+ .selectFrom('users')
71
+ .innerJoin('roles', 'roles.id', 'users.role_id')
72
+ .selectAll('users')
73
+ .select('roles.name as role')
74
+ .orderBy('users.username')
75
+ .execute();
76
+ return rows.map(mapUser);
77
+ }
78
+ export async function updateUser(db, driver, id, patch) {
79
+ const roles = patch.role ? await ensureRoles(db) : undefined;
80
+ await db
81
+ .updateTable('users')
82
+ .set({
83
+ ...(patch.passwordHash ? { password_hash: patch.passwordHash } : {}),
84
+ ...(roles && patch.role ? { role_id: roles[patch.role] } : {}),
85
+ ...(patch.active !== undefined ? { active: enc.bool(driver, patch.active) } : {}),
86
+ ...(patch.email !== undefined ? { email: patch.email } : {}),
87
+ ...(patch.lastLoginAt ? { last_login_at: patch.lastLoginAt } : {}),
88
+ updated_at: nowIso(),
89
+ })
90
+ .where('id', '=', id)
91
+ .execute();
92
+ }
93
+ export async function countUsers(db) {
94
+ const r = await db
95
+ .selectFrom('users')
96
+ .select((eb) => eb.fn.countAll().as('n'))
97
+ .executeTakeFirst();
98
+ return Number(r?.n ?? 0);
99
+ }
100
+ function mapUser(row) {
101
+ return {
102
+ id: row.id,
103
+ username: row.username,
104
+ email: row.email,
105
+ passwordHash: row.password_hash,
106
+ role: row.role,
107
+ active: readBool(row.active),
108
+ authProvider: row.auth_provider,
109
+ lastLoginAt: readTs(row.last_login_at),
110
+ createdAt: readTs(row.created_at),
111
+ };
112
+ }
113
+ // ── sessions ──────────────────────────────────────────────────────────────
114
+ export function hashToken(token) {
115
+ return createHash('sha256').update(token).digest('hex');
116
+ }
117
+ export async function createSession(db, input) {
118
+ const token = randomBytes(32).toString('base64url');
119
+ const csrf = randomBytes(24).toString('base64url');
120
+ const now = nowIso();
121
+ await db
122
+ .insertInto('sessions')
123
+ .values({
124
+ id: hashToken(token),
125
+ user_id: input.userId,
126
+ csrf_token: csrf,
127
+ ip: input.ip ?? null,
128
+ user_agent: input.userAgent ?? null,
129
+ created_at: now,
130
+ expires_at: new Date(Date.now() + input.ttlMs).toISOString(),
131
+ last_seen_at: now,
132
+ })
133
+ .execute();
134
+ return { token, csrfToken: csrf };
135
+ }
136
+ export async function getSession(db, token) {
137
+ const row = await db
138
+ .selectFrom('sessions')
139
+ .selectAll()
140
+ .where('id', '=', hashToken(token))
141
+ .executeTakeFirst();
142
+ if (!row)
143
+ return null;
144
+ if (new Date(String(row.expires_at)).getTime() < Date.now())
145
+ return null;
146
+ return {
147
+ id: row.id,
148
+ userId: row.user_id,
149
+ csrfToken: row.csrf_token,
150
+ expiresAt: readTs(row.expires_at),
151
+ lastSeenAt: readTs(row.last_seen_at),
152
+ };
153
+ }
154
+ export async function touchSession(db, token, ttlMs) {
155
+ await db
156
+ .updateTable('sessions')
157
+ .set({ last_seen_at: nowIso(), expires_at: new Date(Date.now() + ttlMs).toISOString() })
158
+ .where('id', '=', hashToken(token))
159
+ .execute();
160
+ }
161
+ export async function deleteSession(db, token) {
162
+ await db.deleteFrom('sessions').where('id', '=', hashToken(token)).execute();
163
+ }
164
+ export async function purgeExpiredSessions(db) {
165
+ await db.deleteFrom('sessions').where('expires_at', '<', nowIso()).execute();
166
+ }
167
+ // ── api tokens (free, scoped) ─────────────────────────────────────────────
168
+ export async function createApiToken(db, input) {
169
+ const allowed = new Set(scopesForRole(input.ownerRole));
170
+ const scopes = input.scopes.filter((s) => isScope(s));
171
+ const denied = scopes.filter((s) => !allowed.has(s));
172
+ if (denied.length)
173
+ throw new Error(`Scopes exceed the owner's role: ${denied.join(', ')}`);
174
+ const token = API_TOKEN_PREFIX +
175
+ randomBytes(32)
176
+ .toString('base64url')
177
+ .replace(/[^A-Za-z0-9]/g, '')
178
+ .slice(0, 40);
179
+ const id = newId();
180
+ await db
181
+ .insertInto('api_tokens')
182
+ .values({
183
+ id,
184
+ user_id: input.userId,
185
+ name: input.name,
186
+ token_prefix: token.slice(0, 12),
187
+ hash: hashToken(token),
188
+ scopes_json: enc.json(scopes),
189
+ expires_at: input.expiresInDays
190
+ ? new Date(Date.now() + input.expiresInDays * 86_400_000).toISOString()
191
+ : null,
192
+ last_used_at: null,
193
+ revoked_at: null,
194
+ created_at: nowIso(),
195
+ })
196
+ .execute();
197
+ return { token, id, prefix: token.slice(0, 12), scopes };
198
+ }
199
+ /** Resolve a bearer token to its owner and effective scopes (intersection with the owner's current role). */
200
+ export async function resolveApiToken(db, token) {
201
+ if (!token.startsWith(API_TOKEN_PREFIX))
202
+ return null;
203
+ const row = await db
204
+ .selectFrom('api_tokens')
205
+ .selectAll()
206
+ .where('hash', '=', hashToken(token))
207
+ .executeTakeFirst();
208
+ if (!row || row.revoked_at)
209
+ return null;
210
+ if (row.expires_at && new Date(String(row.expires_at)).getTime() < Date.now())
211
+ return null;
212
+ const user = await getUserById(db, row.user_id);
213
+ if (!user || !user.active)
214
+ return null;
215
+ const roleScopes = new Set(scopesForRole(user.role));
216
+ const scopes = (readJson(row.scopes_json) ?? []).filter((s) => isScope(s) && roleScopes.has(s));
217
+ const last = readTs(row.last_used_at);
218
+ if (!last || Date.now() - new Date(last).getTime() > 5 * 60_000) {
219
+ await db
220
+ .updateTable('api_tokens')
221
+ .set({ last_used_at: nowIso() })
222
+ .where('id', '=', row.id)
223
+ .execute();
224
+ }
225
+ return { tokenId: row.id, userId: user.id, username: user.username, role: user.role, scopes };
226
+ }
227
+ export async function listApiTokens(db, userId) {
228
+ let q = db
229
+ .selectFrom('api_tokens')
230
+ .innerJoin('users', 'users.id', 'api_tokens.user_id')
231
+ .selectAll('api_tokens')
232
+ .select('users.username as username')
233
+ .orderBy('api_tokens.created_at', 'desc');
234
+ if (userId)
235
+ q = q.where('api_tokens.user_id', '=', userId);
236
+ const rows = await q.execute();
237
+ return rows.map((r) => ({
238
+ id: r.id,
239
+ userId: r.user_id,
240
+ username: r.username,
241
+ name: r.name,
242
+ prefix: r.token_prefix,
243
+ scopes: readJson(r.scopes_json) ?? [],
244
+ expiresAt: readTs(r.expires_at),
245
+ lastUsedAt: readTs(r.last_used_at),
246
+ revokedAt: readTs(r.revoked_at),
247
+ createdAt: readTs(r.created_at),
248
+ }));
249
+ }
250
+ export async function revokeApiToken(db, id) {
251
+ await db
252
+ .updateTable('api_tokens')
253
+ .set({ revoked_at: nowIso() })
254
+ .where('id', '=', id)
255
+ .where('revoked_at', 'is', null)
256
+ .execute();
257
+ }
258
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,151 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { ModuleConfig, ProcessConfig, ProjectConfig, WorkspaceFile } from '@sdods/contracts/schemas';
3
+ import { type OrgRole, type Role, type WorkspaceRole } from '@sdods/contracts/scopes';
4
+ import type { Driver } from '../driver.js';
5
+ import type { Database } from '../schema.js';
6
+ export declare function upsertOrganization(db: Kysely<Database>, input: {
7
+ slug: string;
8
+ name: string;
9
+ description?: string | null;
10
+ url?: string | null;
11
+ }): Promise<string>;
12
+ export declare function upsertWorkspace(db: Kysely<Database>, input: {
13
+ organizationId: string;
14
+ slug: string;
15
+ name: string;
16
+ description?: string | null;
17
+ }): Promise<string>;
18
+ export declare function listOrganizations(db: Kysely<Database>): Promise<{
19
+ id: string;
20
+ slug: string;
21
+ name: string;
22
+ description: string | null;
23
+ url: string | null;
24
+ createdAt: string | null;
25
+ }[]>;
26
+ export declare function listWorkspaces(db: Kysely<Database>, organizationId?: string): Promise<{
27
+ id: string;
28
+ organizationId: string;
29
+ organizationSlug: string;
30
+ slug: string;
31
+ name: string;
32
+ description: string | null;
33
+ createdAt: string | null;
34
+ }[]>;
35
+ export declare function getWorkspaceBySlug(db: Kysely<Database>, orgSlug: string, slug: string): Promise<{
36
+ id: string;
37
+ organizationId: string;
38
+ organizationSlug: string;
39
+ slug: string;
40
+ name: string;
41
+ description: string | null;
42
+ createdAt: string | null;
43
+ } | null>;
44
+ export declare function addOrgMember(db: Kysely<Database>, organizationId: string, userId: string, role: OrgRole): Promise<void>;
45
+ export declare function removeOrgMember(db: Kysely<Database>, organizationId: string, userId: string): Promise<void>;
46
+ export declare function addWorkspaceMember(db: Kysely<Database>, workspaceId: string, userId: string, role: WorkspaceRole): Promise<void>;
47
+ export declare function removeWorkspaceMember(db: Kysely<Database>, workspaceId: string, userId: string): Promise<void>;
48
+ /** `setRole` for either level. */
49
+ export declare function setRole(db: Kysely<Database>, target: {
50
+ organizationId: string;
51
+ userId: string;
52
+ role: OrgRole;
53
+ } | {
54
+ workspaceId: string;
55
+ userId: string;
56
+ role: WorkspaceRole;
57
+ }): Promise<void>;
58
+ export declare function listOrgMembers(db: Kysely<Database>, organizationId: string): Promise<{
59
+ id: string;
60
+ userId: string;
61
+ username: string;
62
+ role: OrgRole;
63
+ createdAt: string | null;
64
+ }[]>;
65
+ export declare function listWorkspaceMembers(db: Kysely<Database>, workspaceId: string): Promise<{
66
+ id: string;
67
+ userId: string;
68
+ username: string;
69
+ role: WorkspaceRole;
70
+ createdAt: string | null;
71
+ }[]>;
72
+ export declare function listUserOrgs(db: Kysely<Database>, userId: string): Promise<{
73
+ id: string;
74
+ slug: string;
75
+ name: string;
76
+ role: OrgRole;
77
+ }[]>;
78
+ /**
79
+ * Every workspace the user can see with the effective role (max of explicit workspace role and the
80
+ * role implied by org membership; platform admins see everything as admin).
81
+ */
82
+ export declare function listUserWorkspaces(db: Kysely<Database>, userId: string, platformRole?: Role): Promise<({
83
+ id: string;
84
+ organizationId: string;
85
+ organizationSlug: string;
86
+ slug: string;
87
+ name: string;
88
+ description: string | null;
89
+ createdAt: string | null;
90
+ } & {
91
+ role: WorkspaceRole;
92
+ })[]>;
93
+ export declare function effectiveRoleForWorkspace(db: Kysely<Database>, userId: string, workspaceId: string, platformRole?: Role): Promise<WorkspaceRole | null>;
94
+ /** Grant `owner` on every organization that has no owner yet (admin bootstrap). */
95
+ export declare function bootstrapOwner(db: Kysely<Database>, userId: string): Promise<string[]>;
96
+ export declare function upsertModule(db: Kysely<Database>, projectId: string, m: ModuleConfig): Promise<string>;
97
+ export declare function listModules(db: Kysely<Database>, projectId: string): Promise<{
98
+ id: string;
99
+ name: string;
100
+ title: string | null;
101
+ description: string | null;
102
+ path: string | null;
103
+ layers: string[];
104
+ testingTypes: string[];
105
+ tags: string[];
106
+ owner: string | null;
107
+ jiraComponent: string | null;
108
+ routes: string[];
109
+ endpoints: string[];
110
+ }[]>;
111
+ export declare function upsertProcess(db: Kysely<Database>, driver: Driver, scope: {
112
+ projectId?: string | null;
113
+ workspaceId?: string | null;
114
+ }, p: ProcessConfig): Promise<string>;
115
+ export declare function listProcesses(db: Kysely<Database>, scope?: {
116
+ projectId?: string;
117
+ workspaceId?: string;
118
+ }): Promise<{
119
+ id: string;
120
+ projectId: string | null;
121
+ workspaceId: string | null;
122
+ name: string;
123
+ title: string | null;
124
+ description: string | null;
125
+ trigger: string;
126
+ config: Record<string, unknown>;
127
+ enabled: boolean;
128
+ lastRunId: string | null;
129
+ lastStatus: string | null;
130
+ }[]>;
131
+ export declare function updateProcessState(db: Kysely<Database>, id: string, patch: {
132
+ lastRunId?: string | null;
133
+ lastStatus?: string | null;
134
+ }): Promise<void>;
135
+ export interface SyncHierarchyInput {
136
+ workspaceFile?: WorkspaceFile | null;
137
+ projects: Array<{
138
+ config: ProjectConfig;
139
+ root?: string;
140
+ }>;
141
+ }
142
+ export interface SyncHierarchyResult {
143
+ organizationId: string | null;
144
+ workspaces: Record<string, string>;
145
+ projects: Record<string, string>;
146
+ modules: number;
147
+ processes: number;
148
+ }
149
+ /** Upsert organization → workspaces → projects → modules/processes from the yaml files. */
150
+ export declare function syncHierarchy(db: Kysely<Database>, driver: Driver, input: SyncHierarchyInput): Promise<SyncHierarchyResult>;
151
+ //# sourceMappingURL=hierarchy.d.ts.map