postgres-memory-server 0.2.0 → 0.2.1
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.
- package/dist/cli.js +206 -78
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +15 -0
- package/dist/index.js +191 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/PostgresMemoryServer.ts
|
|
2
|
-
import { promises as fs2 } from "fs";
|
|
2
|
+
import { promises as fs2, rmSync } from "fs";
|
|
3
3
|
import os2 from "os";
|
|
4
4
|
import path2 from "path";
|
|
5
|
+
import process2 from "process";
|
|
5
6
|
import EmbeddedPostgres from "embedded-postgres";
|
|
6
7
|
import { Client } from "pg";
|
|
7
8
|
|
|
@@ -62,7 +63,8 @@ function getPgMajorVersion() {
|
|
|
62
63
|
const content = readFileSync(candidate, "utf8");
|
|
63
64
|
const pkg = JSON.parse(content);
|
|
64
65
|
if (pkg.name === "embedded-postgres" && pkg.version) {
|
|
65
|
-
|
|
66
|
+
const major = pkg.version.split(".")[0];
|
|
67
|
+
if (major) return major;
|
|
66
68
|
}
|
|
67
69
|
} catch {
|
|
68
70
|
}
|
|
@@ -208,7 +210,7 @@ function buildDownloadUrl(version, pgMajorVersion, platform, arch) {
|
|
|
208
210
|
}
|
|
209
211
|
function getMacOSCodename() {
|
|
210
212
|
const release = os.release();
|
|
211
|
-
const majorVersion = parseInt(release.split(".")[0], 10);
|
|
213
|
+
const majorVersion = parseInt(release.split(".")[0] ?? "0", 10);
|
|
212
214
|
if (majorVersion >= 24) return "sequoia";
|
|
213
215
|
if (majorVersion >= 23) return "sonoma";
|
|
214
216
|
throw new Error(
|
|
@@ -373,7 +375,7 @@ async function installPgVectorExtension(nativeDir, pgMajorVersion) {
|
|
|
373
375
|
function getHomebrewBottleTag(platform, arch) {
|
|
374
376
|
if (platform === "darwin") {
|
|
375
377
|
const release = os.release();
|
|
376
|
-
const major = parseInt(release.split(".")[0], 10);
|
|
378
|
+
const major = parseInt(release.split(".")[0] ?? "0", 10);
|
|
377
379
|
const prefix = arch === "arm64" ? "arm64_" : "";
|
|
378
380
|
if (major >= 25) return `${prefix}tahoe`;
|
|
379
381
|
if (major >= 24) return `${prefix}sequoia`;
|
|
@@ -385,9 +387,61 @@ function getHomebrewBottleTag(platform, arch) {
|
|
|
385
387
|
}
|
|
386
388
|
throw new Error(`No Homebrew bottles available for ${platform}-${arch}`);
|
|
387
389
|
}
|
|
390
|
+
var ORPHAN_MIN_AGE_MS = 6e4;
|
|
391
|
+
async function sweepOrphanedDataDirs(minAgeMs = ORPHAN_MIN_AGE_MS) {
|
|
392
|
+
const tmpDir = os.tmpdir();
|
|
393
|
+
let entries;
|
|
394
|
+
try {
|
|
395
|
+
entries = await fs.readdir(tmpDir);
|
|
396
|
+
} catch {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const cutoff = Date.now() - minAgeMs;
|
|
400
|
+
await Promise.all(
|
|
401
|
+
entries.filter((name) => name.startsWith("postgres-memory-server-")).map(async (name) => {
|
|
402
|
+
const fullPath = path.join(tmpDir, name);
|
|
403
|
+
let stat;
|
|
404
|
+
try {
|
|
405
|
+
stat = await fs.stat(fullPath);
|
|
406
|
+
if (!stat.isDirectory()) return;
|
|
407
|
+
} catch {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const pidFile = path.join(fullPath, "postmaster.pid");
|
|
411
|
+
let pid = null;
|
|
412
|
+
let pidFileExists = false;
|
|
413
|
+
try {
|
|
414
|
+
const content = await fs.readFile(pidFile, "utf8");
|
|
415
|
+
pidFileExists = true;
|
|
416
|
+
const firstLine = content.split("\n")[0]?.trim();
|
|
417
|
+
const parsed = firstLine ? parseInt(firstLine, 10) : NaN;
|
|
418
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
419
|
+
pid = parsed;
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
if (pid !== null) {
|
|
424
|
+
try {
|
|
425
|
+
process.kill(pid, 0);
|
|
426
|
+
return;
|
|
427
|
+
} catch (err) {
|
|
428
|
+
const code = err.code;
|
|
429
|
+
if (code === "EPERM") {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (!pidFileExists && stat.mtimeMs > cutoff) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
await fs.rm(fullPath, { recursive: true, force: true }).catch(() => {
|
|
438
|
+
});
|
|
439
|
+
})
|
|
440
|
+
);
|
|
441
|
+
}
|
|
388
442
|
function parseParadeDBVersion(version) {
|
|
389
443
|
const match = version.match(/^(\d+\.\d+\.\d+)(?:-pg(\d+))?$/);
|
|
390
|
-
if (!match) {
|
|
444
|
+
if (!match || !match[1]) {
|
|
391
445
|
return { extVersion: version };
|
|
392
446
|
}
|
|
393
447
|
return {
|
|
@@ -473,6 +527,30 @@ function quoteIdentifier(name) {
|
|
|
473
527
|
}
|
|
474
528
|
|
|
475
529
|
// src/PostgresMemoryServer.ts
|
|
530
|
+
var liveInstances = /* @__PURE__ */ new Set();
|
|
531
|
+
var exitHandlersRegistered = false;
|
|
532
|
+
var orphanSweepDone = false;
|
|
533
|
+
function registerExitHandlers() {
|
|
534
|
+
if (exitHandlersRegistered) return;
|
|
535
|
+
exitHandlersRegistered = true;
|
|
536
|
+
const cleanup = () => {
|
|
537
|
+
for (const instance of liveInstances) {
|
|
538
|
+
try {
|
|
539
|
+
instance._cleanupSync();
|
|
540
|
+
} catch {
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
process2.once("exit", cleanup);
|
|
545
|
+
const signalCleanup = (signal) => {
|
|
546
|
+
cleanup();
|
|
547
|
+
process2.removeListener(signal, signalCleanup);
|
|
548
|
+
process2.kill(process2.pid, signal);
|
|
549
|
+
};
|
|
550
|
+
process2.on("SIGINT", signalCleanup);
|
|
551
|
+
process2.on("SIGTERM", signalCleanup);
|
|
552
|
+
process2.on("SIGHUP", signalCleanup);
|
|
553
|
+
}
|
|
476
554
|
var PostgresMemoryServer = class _PostgresMemoryServer {
|
|
477
555
|
constructor(pg, port, dataDir, options) {
|
|
478
556
|
this.pg = pg;
|
|
@@ -485,64 +563,81 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
|
|
|
485
563
|
snapshotSupported;
|
|
486
564
|
hasSnapshot = false;
|
|
487
565
|
static async create(options = {}) {
|
|
566
|
+
if (!orphanSweepDone) {
|
|
567
|
+
orphanSweepDone = true;
|
|
568
|
+
await sweepOrphanedDataDirs().catch(() => {
|
|
569
|
+
});
|
|
570
|
+
}
|
|
488
571
|
const normalized = normalizeOptions(options);
|
|
489
572
|
const port = await getFreePort();
|
|
490
573
|
const dataDir = await fs2.mkdtemp(
|
|
491
574
|
path2.join(os2.tmpdir(), "postgres-memory-server-")
|
|
492
575
|
);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
} catch (error) {
|
|
501
|
-
throw new ExtensionInstallError(
|
|
502
|
-
"pg_search",
|
|
503
|
-
error instanceof Error ? error : new Error(String(error))
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
if (normalized.extensions.includes("vector")) {
|
|
576
|
+
let pg;
|
|
577
|
+
try {
|
|
578
|
+
const postgresFlags = [];
|
|
579
|
+
if (normalized.preset === "paradedb") {
|
|
580
|
+
const nativeDir = getNativeDir();
|
|
581
|
+
const extVersion = resolveParadeDBVersion(normalized.version);
|
|
582
|
+
const pgMajor = DEFAULT_POSTGRES_VERSION;
|
|
507
583
|
try {
|
|
508
|
-
await
|
|
584
|
+
await installParadeDBExtension(nativeDir, extVersion, pgMajor);
|
|
509
585
|
} catch (error) {
|
|
510
586
|
throw new ExtensionInstallError(
|
|
511
|
-
"
|
|
587
|
+
"pg_search",
|
|
512
588
|
error instanceof Error ? error : new Error(String(error))
|
|
513
589
|
);
|
|
514
590
|
}
|
|
591
|
+
if (normalized.extensions.includes("vector")) {
|
|
592
|
+
try {
|
|
593
|
+
await installPgVectorExtension(nativeDir, pgMajor);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
throw new ExtensionInstallError(
|
|
596
|
+
"vector",
|
|
597
|
+
error instanceof Error ? error : new Error(String(error))
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (normalized.extensions.includes("pg_search") || normalized.extensions.length === 0) {
|
|
602
|
+
postgresFlags.push("-c", "shared_preload_libraries=pg_search");
|
|
603
|
+
}
|
|
515
604
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
605
|
+
pg = new EmbeddedPostgres({
|
|
606
|
+
databaseDir: dataDir,
|
|
607
|
+
port,
|
|
608
|
+
user: normalized.username,
|
|
609
|
+
password: normalized.password,
|
|
610
|
+
persistent: false,
|
|
611
|
+
postgresFlags,
|
|
612
|
+
onLog: () => {
|
|
613
|
+
},
|
|
614
|
+
onError: () => {
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
await pg.initialise();
|
|
618
|
+
await pg.start();
|
|
619
|
+
if (normalized.database !== "postgres") {
|
|
620
|
+
await pg.createDatabase(normalized.database);
|
|
521
621
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
persistent: false,
|
|
529
|
-
postgresFlags,
|
|
530
|
-
onLog: () => {
|
|
531
|
-
},
|
|
532
|
-
onError: () => {
|
|
622
|
+
const server = new _PostgresMemoryServer(pg, port, dataDir, normalized);
|
|
623
|
+
liveInstances.add(server);
|
|
624
|
+
registerExitHandlers();
|
|
625
|
+
const initStatements = buildInitStatements(normalized);
|
|
626
|
+
if (initStatements.length > 0) {
|
|
627
|
+
await server.runSql(initStatements);
|
|
533
628
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
629
|
+
return server;
|
|
630
|
+
} catch (error) {
|
|
631
|
+
if (pg) {
|
|
632
|
+
try {
|
|
633
|
+
await pg.stop();
|
|
634
|
+
} catch {
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
await fs2.rm(dataDir, { recursive: true, force: true }).catch(() => {
|
|
638
|
+
});
|
|
639
|
+
throw error;
|
|
544
640
|
}
|
|
545
|
-
return server;
|
|
546
641
|
}
|
|
547
642
|
static createPostgres(options = {}) {
|
|
548
643
|
return _PostgresMemoryServer.create({ ...options, preset: "postgres" });
|
|
@@ -574,6 +669,14 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
|
|
|
574
669
|
getImage() {
|
|
575
670
|
return this.options.image;
|
|
576
671
|
}
|
|
672
|
+
/**
|
|
673
|
+
* Returns the absolute path to the temporary PostgreSQL data directory
|
|
674
|
+
* for this instance. Useful for debugging or backing up state. The
|
|
675
|
+
* directory is automatically removed by `stop()`.
|
|
676
|
+
*/
|
|
677
|
+
getDataDir() {
|
|
678
|
+
return this.dataDir;
|
|
679
|
+
}
|
|
577
680
|
getConnectionOptions() {
|
|
578
681
|
return {
|
|
579
682
|
host: this.getHost(),
|
|
@@ -679,7 +782,32 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
|
|
|
679
782
|
return;
|
|
680
783
|
}
|
|
681
784
|
this.stopped = true;
|
|
682
|
-
|
|
785
|
+
liveInstances.delete(this);
|
|
786
|
+
try {
|
|
787
|
+
await this.pg.stop();
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
await fs2.rm(this.dataDir, { recursive: true, force: true }).catch(() => {
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Synchronous cleanup for use in process exit handlers. Cannot await,
|
|
795
|
+
* so we just remove the data directory and let the OS reap the postgres
|
|
796
|
+
* child process. embedded-postgres registers its own exit hook to kill
|
|
797
|
+
* the process; this method is a backup for the data directory only.
|
|
798
|
+
*
|
|
799
|
+
* @internal
|
|
800
|
+
*/
|
|
801
|
+
_cleanupSync() {
|
|
802
|
+
if (this.stopped) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
this.stopped = true;
|
|
806
|
+
liveInstances.delete(this);
|
|
807
|
+
try {
|
|
808
|
+
rmSync(this.dataDir, { recursive: true, force: true });
|
|
809
|
+
} catch {
|
|
810
|
+
}
|
|
683
811
|
}
|
|
684
812
|
/**
|
|
685
813
|
* Connect to the "postgres" system database for admin operations
|
|
@@ -718,7 +846,7 @@ import { spawn } from "child_process";
|
|
|
718
846
|
import { createHash } from "crypto";
|
|
719
847
|
import path3 from "path";
|
|
720
848
|
import { tmpdir } from "os";
|
|
721
|
-
import
|
|
849
|
+
import process3 from "process";
|
|
722
850
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
723
851
|
var CHILD_OPTIONS_ENV_VAR = "POSTGRES_MEMORY_SERVER_CHILD_OPTIONS_B64";
|
|
724
852
|
var CHILD_SETUP_TIMEOUT_MS = 12e4;
|
|
@@ -727,7 +855,7 @@ var POLL_INTERVAL_MS = 100;
|
|
|
727
855
|
var DEFAULT_JEST_ENV_VAR_NAME = "DATABASE_URL";
|
|
728
856
|
var DEFAULT_JEST_STATE_FILE = path3.join(
|
|
729
857
|
tmpdir(),
|
|
730
|
-
`postgres-memory-server-jest-${createHash("sha256").update(
|
|
858
|
+
`postgres-memory-server-jest-${createHash("sha256").update(process3.cwd()).digest("hex").slice(0, 12)}.json`
|
|
731
859
|
);
|
|
732
860
|
function getChildScript(childModuleUrl) {
|
|
733
861
|
return `
|
|
@@ -823,24 +951,24 @@ async function readStateFile(filePath) {
|
|
|
823
951
|
}
|
|
824
952
|
}
|
|
825
953
|
function applyConnectionEnvironment(envVarName, payload) {
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
954
|
+
process3.env[envVarName] = payload.uri;
|
|
955
|
+
process3.env.POSTGRES_MEMORY_SERVER_URI = payload.uri;
|
|
956
|
+
process3.env.POSTGRES_MEMORY_SERVER_HOST = payload.host;
|
|
957
|
+
process3.env.POSTGRES_MEMORY_SERVER_PORT = String(payload.port);
|
|
958
|
+
process3.env.POSTGRES_MEMORY_SERVER_DATABASE = payload.database;
|
|
959
|
+
process3.env.POSTGRES_MEMORY_SERVER_USERNAME = payload.username;
|
|
960
|
+
process3.env.POSTGRES_MEMORY_SERVER_PASSWORD = payload.password;
|
|
961
|
+
process3.env.POSTGRES_MEMORY_SERVER_IMAGE = payload.image;
|
|
834
962
|
}
|
|
835
963
|
async function startChildProcess(options) {
|
|
836
964
|
const childModuleUrl = await resolveChildModuleUrl();
|
|
837
965
|
return new Promise((resolve, reject) => {
|
|
838
966
|
const child = spawn(
|
|
839
|
-
|
|
967
|
+
process3.execPath,
|
|
840
968
|
["--input-type=module", "--eval", getChildScript(childModuleUrl)],
|
|
841
969
|
{
|
|
842
970
|
env: {
|
|
843
|
-
...
|
|
971
|
+
...process3.env,
|
|
844
972
|
[CHILD_OPTIONS_ENV_VAR]: Buffer.from(
|
|
845
973
|
JSON.stringify(options),
|
|
846
974
|
"utf8"
|
|
@@ -936,7 +1064,7 @@ async function resolveChildModuleUrl() {
|
|
|
936
1064
|
}
|
|
937
1065
|
async function stopChildProcess(pid) {
|
|
938
1066
|
try {
|
|
939
|
-
|
|
1067
|
+
process3.kill(pid, "SIGTERM");
|
|
940
1068
|
} catch (error) {
|
|
941
1069
|
if (isMissingProcessError(error)) {
|
|
942
1070
|
return;
|
|
@@ -947,7 +1075,7 @@ async function stopChildProcess(pid) {
|
|
|
947
1075
|
while (Date.now() < deadline) {
|
|
948
1076
|
await sleep(POLL_INTERVAL_MS);
|
|
949
1077
|
try {
|
|
950
|
-
|
|
1078
|
+
process3.kill(pid, 0);
|
|
951
1079
|
} catch (error) {
|
|
952
1080
|
if (isMissingProcessError(error)) {
|
|
953
1081
|
return;
|