|
1 | | -import { spawn } from 'node:child_process' |
2 | 1 | import { mkdir } from 'node:fs/promises' |
3 | 2 | import { createClient } from '@libsql/client' |
4 | 3 | import { devDatabaseDir, testDatabaseFile } from './paths' |
5 | 4 |
|
6 | | -const MIGRATION_VERSION = '2024-03-12' |
| 5 | +const seededConsentSubjectOne = '00000000-0000-0000-0000-000000000001' |
| 6 | +const seededConsentSubjectTwo = '00000000-0000-0000-0000-000000000002' |
7 | 7 |
|
8 | | -const runCommand = (command: string, args: string[], env?: NodeJS.ProcessEnv) => { |
9 | | - return new Promise<void>((resolve, reject) => { |
10 | | - const child = spawn(command, args, { |
11 | | - stdio: 'inherit', |
12 | | - env: env ?? process.env, |
13 | | - shell: process.platform === 'win32', |
14 | | - }) |
15 | | - |
16 | | - child.on('error', (error) => { |
17 | | - reject(error) |
18 | | - }) |
| 8 | +const seededConsentSubjectIds = [seededConsentSubjectOne, seededConsentSubjectTwo] |
19 | 9 |
|
20 | | - child.on('exit', (code) => { |
21 | | - if (code === 0) { |
22 | | - resolve() |
23 | | - } else { |
24 | | - reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`)) |
25 | | - } |
26 | | - }) |
27 | | - }) |
| 10 | +type SeededConsentRow = { |
| 11 | + id: string |
| 12 | + dataSubjectId: string |
| 13 | + email: string | null |
| 14 | + purposes: string |
| 15 | + source: string |
| 16 | + userAgent: string |
| 17 | + ipAddress: string | null |
| 18 | + privacyPolicyVersion: string |
| 19 | + consentText: string | null |
| 20 | + verified: number |
28 | 21 | } |
29 | 22 |
|
30 | | -export const resolveTestDatabaseFile = () => testDatabaseFile |
| 23 | +const seededConsentRows: SeededConsentRow[] = [ |
| 24 | + { |
| 25 | + id: 'seed-consent-1', |
| 26 | + dataSubjectId: seededConsentSubjectOne, |
| 27 | + email: 'seed@example.com', |
| 28 | + purposes: JSON.stringify(['analytics', 'contact']), |
| 29 | + source: 'seed_script', |
| 30 | + userAgent: 'seed-agent', |
| 31 | + ipAddress: '127.0.0.1', |
| 32 | + privacyPolicyVersion: 'seed', |
| 33 | + consentText: 'Seeded consent record', |
| 34 | + verified: 1, |
| 35 | + }, |
| 36 | + { |
| 37 | + id: 'seed-consent-2', |
| 38 | + dataSubjectId: seededConsentSubjectTwo, |
| 39 | + email: null, |
| 40 | + purposes: JSON.stringify(['marketing']), |
| 41 | + source: 'seed_script', |
| 42 | + userAgent: 'seed-agent', |
| 43 | + ipAddress: null, |
| 44 | + privacyPolicyVersion: 'seed', |
| 45 | + consentText: null, |
| 46 | + verified: 0, |
| 47 | + }, |
| 48 | +] |
31 | 49 |
|
32 | | -const ensureSnapshotTableHasRow = async (databaseUrl: string, authToken?: string) => { |
| 50 | +const ensureRequiredTables = async (databaseUrl: string, authToken?: string) => { |
33 | 51 | const client = createClient( |
34 | 52 | authToken ? { url: databaseUrl, authToken } : { url: databaseUrl } |
35 | 53 | ) |
36 | 54 |
|
37 | 55 | try { |
38 | 56 | await client.execute(` |
39 | | - CREATE TABLE IF NOT EXISTS _astro_db_snapshot ( |
40 | | - id INTEGER PRIMARY KEY AUTOINCREMENT, |
41 | | - version TEXT, |
42 | | - snapshot BLOB |
| 57 | + CREATE TABLE IF NOT EXISTS consentEvents ( |
| 58 | + id TEXT PRIMARY KEY, |
| 59 | + dataSubjectId TEXT NOT NULL, |
| 60 | + email TEXT, |
| 61 | + purposes TEXT NOT NULL, |
| 62 | + source TEXT NOT NULL, |
| 63 | + userAgent TEXT NOT NULL, |
| 64 | + ipAddress TEXT, |
| 65 | + privacyPolicyVersion TEXT NOT NULL, |
| 66 | + consentText TEXT, |
| 67 | + verified INTEGER NOT NULL DEFAULT 0, |
| 68 | + createdAt TEXT NOT NULL |
| 69 | + ); |
| 70 | + `) |
| 71 | + |
| 72 | + await client.execute(` |
| 73 | + CREATE INDEX IF NOT EXISTS consentEvents_subjectCreatedAt |
| 74 | + ON consentEvents (dataSubjectId, createdAt); |
| 75 | + `) |
| 76 | + |
| 77 | + await client.execute(` |
| 78 | + CREATE INDEX IF NOT EXISTS consentEvents_ipCreatedAt |
| 79 | + ON consentEvents (ipAddress, createdAt); |
| 80 | + `) |
| 81 | + |
| 82 | + await client.execute(` |
| 83 | + CREATE TABLE IF NOT EXISTS rateLimitWindows ( |
| 84 | + id TEXT PRIMARY KEY, |
| 85 | + scope TEXT NOT NULL, |
| 86 | + identifier TEXT NOT NULL, |
| 87 | + hits INTEGER NOT NULL, |
| 88 | + "limit" INTEGER NOT NULL, |
| 89 | + windowMs INTEGER NOT NULL, |
| 90 | + windowExpiresAt INTEGER NOT NULL, |
| 91 | + updatedAt TEXT NOT NULL |
| 92 | + ); |
| 93 | + `) |
| 94 | + |
| 95 | + await client.execute(` |
| 96 | + CREATE UNIQUE INDEX IF NOT EXISTS rateLimitWindows_scopeIdentifier |
| 97 | + ON rateLimitWindows (scope, identifier); |
| 98 | + `) |
| 99 | + |
| 100 | + await client.execute(` |
| 101 | + CREATE TABLE IF NOT EXISTS dsarRequests ( |
| 102 | + id TEXT PRIMARY KEY, |
| 103 | + token TEXT NOT NULL, |
| 104 | + email TEXT NOT NULL, |
| 105 | + requestType TEXT NOT NULL, |
| 106 | + expiresAt TEXT NOT NULL, |
| 107 | + fulfilledAt TEXT, |
| 108 | + createdAt TEXT NOT NULL |
| 109 | + ); |
| 110 | + `) |
| 111 | + |
| 112 | + await client.execute(` |
| 113 | + CREATE UNIQUE INDEX IF NOT EXISTS dsarRequests_token |
| 114 | + ON dsarRequests (token); |
| 115 | + `) |
| 116 | + |
| 117 | + await client.execute(` |
| 118 | + CREATE INDEX IF NOT EXISTS dsarRequests_emailRequestType |
| 119 | + ON dsarRequests (email, requestType); |
| 120 | + `) |
| 121 | + |
| 122 | + await client.execute(` |
| 123 | + CREATE TABLE IF NOT EXISTS newsletterConfirmations ( |
| 124 | + id TEXT PRIMARY KEY, |
| 125 | + token TEXT NOT NULL, |
| 126 | + email TEXT NOT NULL, |
| 127 | + dataSubjectId TEXT NOT NULL, |
| 128 | + firstName TEXT, |
| 129 | + source TEXT NOT NULL, |
| 130 | + userAgent TEXT, |
| 131 | + ipAddress TEXT, |
| 132 | + consentTimestamp TEXT NOT NULL, |
| 133 | + expiresAt TEXT NOT NULL, |
| 134 | + confirmedAt TEXT, |
| 135 | + createdAt TEXT NOT NULL |
43 | 136 | ); |
44 | 137 | `) |
45 | 138 |
|
46 | | - const result = await client.execute( |
47 | | - 'SELECT snapshot FROM _astro_db_snapshot ORDER BY id DESC LIMIT 1' |
48 | | - ) |
| 139 | + await client.execute(` |
| 140 | + CREATE UNIQUE INDEX IF NOT EXISTS newsletterConfirmations_token |
| 141 | + ON newsletterConfirmations (token); |
| 142 | + `) |
| 143 | + |
| 144 | + await client.execute(` |
| 145 | + CREATE INDEX IF NOT EXISTS newsletterConfirmations_emailSubject |
| 146 | + ON newsletterConfirmations (email, dataSubjectId); |
| 147 | + `) |
| 148 | + } finally { |
| 149 | + client.close() |
| 150 | + } |
| 151 | +} |
| 152 | + |
| 153 | +const seedRequiredRows = async (databaseUrl: string, authToken?: string) => { |
| 154 | + const client = createClient( |
| 155 | + authToken ? { url: databaseUrl, authToken } : { url: databaseUrl } |
| 156 | + ) |
49 | 157 |
|
50 | | - if (result.rows.length === 0) { |
51 | | - const tablesToReset = [ |
52 | | - 'consentEvents', |
53 | | - 'rateLimitWindows', |
54 | | - 'dsarRequests', |
55 | | - 'newsletterConfirmations', |
56 | | - ] |
| 158 | + try { |
| 159 | + const nowIso = new Date().toISOString() |
| 160 | + const windowExpiresAt = Date.now() + 60000 |
57 | 161 |
|
58 | | - for (const table of tablesToReset) { |
59 | | - await client.execute(`DROP TABLE IF EXISTS "${table}"`) |
60 | | - } |
| 162 | + for (const subjectId of seededConsentSubjectIds) { |
| 163 | + await client.execute({ |
| 164 | + sql: 'DELETE FROM consentEvents WHERE dataSubjectId = ?', |
| 165 | + args: [subjectId], |
| 166 | + }) |
| 167 | + } |
61 | 168 |
|
62 | | - const snapshot = JSON.stringify({ version: MIGRATION_VERSION, schema: {} }) |
| 169 | + await client.execute({ |
| 170 | + sql: 'DELETE FROM rateLimitWindows WHERE scope = ? AND identifier = ?', |
| 171 | + args: ['seed', 'local'], |
| 172 | + }) |
63 | 173 |
|
| 174 | + for (const row of seededConsentRows) { |
64 | 175 | await client.execute({ |
65 | | - sql: 'INSERT INTO _astro_db_snapshot (version, snapshot) VALUES (?, ?)', |
66 | | - args: [MIGRATION_VERSION, snapshot], |
| 176 | + sql: ` |
| 177 | + INSERT INTO consentEvents ( |
| 178 | + id, dataSubjectId, email, purposes, source, userAgent, |
| 179 | + ipAddress, privacyPolicyVersion, consentText, verified, createdAt |
| 180 | + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| 181 | + `, |
| 182 | + args: [ |
| 183 | + row.id, |
| 184 | + row.dataSubjectId, |
| 185 | + row.email ?? null, |
| 186 | + row.purposes, |
| 187 | + row.source, |
| 188 | + row.userAgent, |
| 189 | + row.ipAddress ?? null, |
| 190 | + row.privacyPolicyVersion, |
| 191 | + row.consentText ?? null, |
| 192 | + row.verified, |
| 193 | + nowIso, |
| 194 | + ], |
67 | 195 | }) |
68 | 196 | } |
| 197 | + |
| 198 | + await client.execute({ |
| 199 | + sql: ` |
| 200 | + INSERT INTO rateLimitWindows ( |
| 201 | + id, scope, identifier, hits, "limit", windowMs, windowExpiresAt, updatedAt |
| 202 | + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) |
| 203 | + `, |
| 204 | + args: [ |
| 205 | + 'seed-rate-limit-1', |
| 206 | + 'seed', |
| 207 | + 'local', |
| 208 | + 0, |
| 209 | + 1, |
| 210 | + 60000, |
| 211 | + windowExpiresAt, |
| 212 | + nowIso, |
| 213 | + ], |
| 214 | + }) |
69 | 215 | } finally { |
70 | 216 | client.close() |
71 | 217 | } |
72 | 218 | } |
73 | 219 |
|
74 | | -const runAstroDb = async (args: string[], env: NodeJS.ProcessEnv) => { |
75 | | - await runCommand('npx', ['astro', 'db', ...args], env) |
76 | | -} |
| 220 | +export const resolveTestDatabaseFile = () => testDatabaseFile |
77 | 221 |
|
78 | 222 | export const prepareDatabase = async () => { |
79 | 223 | await mkdir(devDatabaseDir, { recursive: true }) |
80 | | - // The dev server and Playwright share the same DB file; do not delete it here. |
81 | 224 | const databaseUrl = `file:${testDatabaseFile}` |
82 | | - const env = { |
83 | | - ...process.env, |
84 | | - ASTRO_DATABASE_FILE: testDatabaseFile, |
85 | | - ASTRO_DB_REMOTE_URL: databaseUrl, |
86 | | - } |
87 | | - await ensureSnapshotTableHasRow(databaseUrl, process.env['ASTRO_DB_APP_TOKEN']) |
88 | | - await runAstroDb(['push'], env) |
89 | | - await runAstroDb(['execute', 'db/seed.ts'], env) |
| 225 | + const authToken = process.env['ASTRO_DB_APP_TOKEN'] |
| 226 | + |
| 227 | + // Keep E2E bootstrap independent from Astro DB CLI behavior across Astro versions. |
| 228 | + await ensureRequiredTables(databaseUrl, authToken) |
| 229 | + await seedRequiredRows(databaseUrl, authToken) |
90 | 230 | } |
0 commit comments