Skip to content

Commit 8c57a29

Browse files
committed
Stop depending on astro db CLI during global setup and instead create/seed the required tables directly in SQLite
1 parent 8044474 commit 8c57a29

1 file changed

Lines changed: 195 additions & 55 deletions

File tree

Lines changed: 195 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,230 @@
1-
import { spawn } from 'node:child_process'
21
import { mkdir } from 'node:fs/promises'
32
import { createClient } from '@libsql/client'
43
import { devDatabaseDir, testDatabaseFile } from './paths'
54

6-
const MIGRATION_VERSION = '2024-03-12'
5+
const seededConsentSubjectOne = '00000000-0000-0000-0000-000000000001'
6+
const seededConsentSubjectTwo = '00000000-0000-0000-0000-000000000002'
77

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]
199

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
2821
}
2922

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+
]
3149

32-
const ensureSnapshotTableHasRow = async (databaseUrl: string, authToken?: string) => {
50+
const ensureRequiredTables = async (databaseUrl: string, authToken?: string) => {
3351
const client = createClient(
3452
authToken ? { url: databaseUrl, authToken } : { url: databaseUrl }
3553
)
3654

3755
try {
3856
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
43136
);
44137
`)
45138

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+
)
49157

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
57161

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+
}
61168

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+
})
63173

174+
for (const row of seededConsentRows) {
64175
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+
],
67195
})
68196
}
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+
})
69215
} finally {
70216
client.close()
71217
}
72218
}
73219

74-
const runAstroDb = async (args: string[], env: NodeJS.ProcessEnv) => {
75-
await runCommand('npx', ['astro', 'db', ...args], env)
76-
}
220+
export const resolveTestDatabaseFile = () => testDatabaseFile
77221

78222
export const prepareDatabase = async () => {
79223
await mkdir(devDatabaseDir, { recursive: true })
80-
// The dev server and Playwright share the same DB file; do not delete it here.
81224
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)
90230
}

0 commit comments

Comments
 (0)