Skip to content

Commit 25962c5

Browse files
tmmampagent
andcommitted
feat(zbugs-swift): a backend that keeps its database across restarts
server.ts recreates the database on every start and signs tokens with new keys, so each restart sends every app that synced back to an empty store and logs it out. With ZBUGS_KEEP=1 it keeps the database (where zero-cache keeps the client groups), the replica, and the keys in ZBUGS_DIR, and seeds only a database that doesn't exist yet. Amp-Thread-ID: https://ampcode.com/threads/T-01a1557f-dff8-761e-ad04-7fc6c1bd83db Co-authored-by: Amp <amp@ampcode.com>
1 parent 87c4f43 commit 25962c5

2 files changed

Lines changed: 81 additions & 26 deletions

File tree

‎apps/zbugs-swift/README.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ ZBUGS_PG=postgresql://user:password@127.0.0.1:6434/postgres \
2525
It recreates the `zbugs_swift` database with zbugs' seed data, then prints a
2626
token for a crew user and `ready <zero-cache> <API>` (ports 4848 and 5173).
2727

28+
Each start makes new signing keys and a new database, so an app that ran
29+
against the last one needs the new token and starts over from an empty local
30+
store. To keep both across restarts, set `ZBUGS_KEEP=1` and a directory for
31+
the replica and the keys:
32+
33+
```sh
34+
ZBUGS_KEEP=1 ZBUGS_DIR=~/.zbugs-swift ZBUGS_PG=... \
35+
node apps/zbugs-swift/server/server.ts
36+
```
37+
38+
It seeds the database only when it doesn't exist; drop it, or run once
39+
without `ZBUGS_KEEP`, to start over.
40+
2841
Then generate the Xcode project and run the `ZBugsApp` scheme on a Mac or an
2942
iOS simulator:
3043

‎apps/zbugs-swift/server/server.ts‎

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
* Runs zbugs' backend for the Swift app, in development and in the
33
* end-to-end tests: a fresh Postgres database with zbugs' migrations and
44
* seed data, zbugs' API server (apps/zbugs/api), and zero-cache, all from
5-
* this repository's sources.
5+
* this repository's sources. With `ZBUGS_KEEP=1` it keeps the database, the
6+
* replica, and its signing keys from the last run instead, so an app keeps
7+
* its local data and its token across restarts.
68
*
79
* node apps/zbugs-swift/server/server.ts
810
*
@@ -22,9 +24,18 @@
2224
* ZBUGS_PORT zero-cache port (default 4848)
2325
* ZBUGS_API_PORT API server port (default 5173)
2426
* ZBUGS_DIR working directory for the replica (default a temp dir)
27+
* ZBUGS_KEEP 1 to keep the last run's database, and the replica and
28+
* keys in ZBUGS_DIR, which it needs
2529
*/
2630
import {execFileSync, spawn} from 'node:child_process';
27-
import {mkdtempSync, readdirSync} from 'node:fs';
31+
import {
32+
existsSync,
33+
mkdtempSync,
34+
readFileSync,
35+
readdirSync,
36+
rmSync,
37+
writeFileSync,
38+
} from 'node:fs';
2839
import {registerHooks} from 'node:module';
2940
import {networkInterfaces, tmpdir} from 'node:os';
3041
import {join} from 'node:path';
@@ -42,6 +53,13 @@ const localHost = everywhere ? '127.0.0.1' : host;
4253
const port = Number(env['ZBUGS_PORT'] ?? 4848);
4354
const apiPort = Number(env['ZBUGS_API_PORT'] ?? 5173);
4455
const workDir = env['ZBUGS_DIR'] ?? mkdtempSync(join(tmpdir(), 'zbugs-swift-'));
56+
const keep = env['ZBUGS_KEEP'] === '1';
57+
if (keep && !env['ZBUGS_DIR']) {
58+
process.stderr.write(
59+
'ZBUGS_KEEP needs ZBUGS_DIR, for the replica and keys\n',
60+
);
61+
process.exit(2);
62+
}
4563

4664
const here = (path: string) => fileURLToPath(new URL(path, import.meta.url));
4765
const zbugs = here('../../zbugs/');
@@ -77,29 +95,43 @@ function psql(url: string, ...args: string[]) {
7795
});
7896
}
7997

80-
// The database: zbugs' migrations, then its seed script.
81-
psql(adminURL, '-c', `DROP DATABASE IF EXISTS ${dbName} WITH (FORCE)`);
82-
psql(adminURL, '-c', `CREATE DATABASE ${dbName}`);
83-
const migrations = join(zbugs, 'db/migrations');
84-
for (const file of readdirSync(migrations)
85-
.filter(f => f.endsWith('.sql'))
86-
.sort()) {
87-
// In a transaction each, as drizzle-kit applies them.
88-
psql(upstreamURL, '--single-transaction', '-f', join(migrations, file));
98+
// The database: zbugs' migrations, then its seed script. Kept, it also
99+
// holds zero-cache's client groups, which an app's local data belongs to.
100+
const kept =
101+
keep &&
102+
execFileSync(
103+
'psql',
104+
[adminURL, '-tAc', `SELECT 1 FROM pg_database WHERE datname = '${dbName}'`],
105+
{encoding: 'utf8'},
106+
).trim() === '1';
107+
if (!kept) {
108+
psql(adminURL, '-c', `DROP DATABASE IF EXISTS ${dbName} WITH (FORCE)`);
109+
psql(adminURL, '-c', `CREATE DATABASE ${dbName}`);
110+
const migrations = join(zbugs, 'db/migrations');
111+
for (const file of readdirSync(migrations)
112+
.filter(f => f.endsWith('.sql'))
113+
.sort()) {
114+
// In a transaction each, as drizzle-kit applies them.
115+
psql(upstreamURL, '--single-transaction', '-f', join(migrations, file));
116+
}
117+
execFileSync(process.execPath, [join(zbugs, 'scripts/seed.ts')], {
118+
cwd: zbugs,
119+
stdio: ['ignore', 'ignore', 'inherit'],
120+
env: {
121+
...env,
122+
['ZERO_UPSTREAM_DB']: upstreamURL,
123+
['ZERO_SEED_DATA_DIR']: join(
124+
zbugs,
125+
'db/seed-data',
126+
env['ZBUGS_SEED'] ?? 'github',
127+
),
128+
},
129+
});
130+
// A replica of an earlier database.
131+
for (const file of readdirSync(workDir)) {
132+
if (file.startsWith('replica.db')) rmSync(join(workDir, file));
133+
}
89134
}
90-
execFileSync(process.execPath, [join(zbugs, 'scripts/seed.ts')], {
91-
cwd: zbugs,
92-
stdio: ['ignore', 'ignore', 'inherit'],
93-
env: {
94-
...env,
95-
['ZERO_UPSTREAM_DB']: upstreamURL,
96-
['ZERO_SEED_DATA_DIR']: join(
97-
zbugs,
98-
'db/seed-data',
99-
env['ZBUGS_SEED'] ?? 'github',
100-
),
101-
},
102-
});
103135
const crewID = execFileSync(
104136
'psql',
105137
[
@@ -110,8 +142,18 @@ const crewID = execFileSync(
110142
{encoding: 'utf8'},
111143
).trim();
112144

113-
// The API server, as `pnpm dev` runs it in apps/zbugs, with keys of its own.
114-
const {privateJwk, publicJwk} = await createJwkPair();
145+
// The API server, as `pnpm dev` runs it in apps/zbugs, with keys of its own:
146+
// the last run's, kept, so that the tokens it printed still work.
147+
const keysFile = join(workDir, 'keys.json');
148+
const {privateJwk, publicJwk} =
149+
keep && existsSync(keysFile)
150+
? (JSON.parse(readFileSync(keysFile, 'utf8')) as Awaited<
151+
ReturnType<typeof createJwkPair>
152+
>)
153+
: await createJwkPair();
154+
if (keep) {
155+
writeFileSync(keysFile, JSON.stringify({privateJwk, publicJwk}));
156+
}
115157
env['ZERO_UPSTREAM_DB'] = upstreamURL;
116158
env['PRIVATE_JWK'] = JSON.stringify(privateJwk);
117159
env['VITE_PUBLIC_JWK'] = JSON.stringify(publicJwk);

0 commit comments

Comments
 (0)