pg-here 0.1.0 → 0.1.3
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/README.md +34 -3
- package/dist/index.d.ts +12 -2
- package/dist/index.js +109 -7
- package/index.ts +270 -0
- package/package.json +7 -1
- package/scripts/pg-dev.mjs +45 -0
package/README.md
CHANGED
|
@@ -23,6 +23,32 @@ postgresql://postgres:postgres@localhost:55432/postgres
|
|
|
23
23
|
|
|
24
24
|
Connect from your app using that connection string, then Ctrl+C to stop.
|
|
25
25
|
|
|
26
|
+
### One-command start from npm (no local install)
|
|
27
|
+
|
|
28
|
+
If you have Bun installed, you can run the published package directly:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bunx pg-here
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If this command currently fails with `could not determine executable to run for package`, you’re using a release that was published before the CLI binary was exposed. It will work after the next publish.
|
|
35
|
+
|
|
36
|
+
This starts a local PostgreSQL instance in your current project directory and prints the connection string, then keeps the process alive until you stop it.
|
|
37
|
+
|
|
38
|
+
`bunx pg-here` uses these defaults if you pass nothing:
|
|
39
|
+
- `username=postgres`
|
|
40
|
+
- `password=postgres`
|
|
41
|
+
- `database=postgres`
|
|
42
|
+
- `port=55432`
|
|
43
|
+
|
|
44
|
+
All args are optional.
|
|
45
|
+
|
|
46
|
+
Pass CLI flags just like the local script when you want to override defaults:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bunx pg-here --username postgres --password postgres --database my_app --port 55432
|
|
50
|
+
```
|
|
51
|
+
|
|
26
52
|
### Programmatic usage
|
|
27
53
|
|
|
28
54
|
Use `pg-here` directly from your server startup code and auto-create the app database if missing.
|
|
@@ -47,21 +73,26 @@ await pgHere.stop();
|
|
|
47
73
|
`databaseConnectionString` points to your target DB (`my_app` above).
|
|
48
74
|
If the DB does not exist yet, `createDatabaseIfMissing: true` creates it on startup.
|
|
49
75
|
Set `postgresVersion` if you want to pin/select a specific PostgreSQL version.
|
|
76
|
+
By default, `startPgHere()` installs SIGINT/SIGTERM shutdown hooks that stop Postgres when
|
|
77
|
+
your process exits, and `stop()` preserves data (no cluster cleanup/delete).
|
|
78
|
+
Use `await pgHere.cleanup()` only when you explicitly want full resource cleanup.
|
|
79
|
+
`pg_stat_statements` is enabled automatically (`shared_preload_libraries` + extension creation).
|
|
80
|
+
Set `enablePgStatStatements: false` to opt out.
|
|
50
81
|
|
|
51
82
|
### CLI Options
|
|
52
83
|
|
|
53
84
|
```bash
|
|
54
85
|
# Custom credentials
|
|
55
|
-
bun run db:up --username
|
|
86
|
+
bun run db:up --username postgres --password postgres
|
|
56
87
|
|
|
57
88
|
# Short flags
|
|
58
|
-
bun run db:up -u
|
|
89
|
+
bun run db:up -u postgres -p postgres
|
|
59
90
|
|
|
60
91
|
# Custom port
|
|
61
92
|
bun run db:up --port 55433
|
|
62
93
|
|
|
63
94
|
# All together
|
|
64
|
-
bun run db:up -u
|
|
95
|
+
bun run db:up -u postgres -p postgres --database postgres --port 55433
|
|
65
96
|
|
|
66
97
|
# Pin postgres version
|
|
67
98
|
bun run db:up --pg-version 18.0.0
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PostgresInstance } from "pg-embedded";
|
|
2
|
+
export type PgHereShutdownSignal = "SIGINT" | "SIGTERM" | "SIGHUP";
|
|
2
3
|
export interface PgHereOptions {
|
|
3
4
|
projectDir?: string;
|
|
4
5
|
dataDir?: string;
|
|
@@ -11,16 +12,25 @@ export interface PgHereOptions {
|
|
|
11
12
|
persistent?: boolean;
|
|
12
13
|
database?: string;
|
|
13
14
|
createDatabaseIfMissing?: boolean;
|
|
15
|
+
registerProcessShutdownHandlers?: boolean;
|
|
16
|
+
shutdownSignals?: PgHereShutdownSignal[];
|
|
17
|
+
cleanupOnShutdown?: boolean;
|
|
18
|
+
enablePgStatStatements?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface StopPgHereOptions {
|
|
21
|
+
cleanup?: boolean;
|
|
14
22
|
}
|
|
15
23
|
export interface PgHereHandle {
|
|
16
24
|
instance: PostgresInstance;
|
|
17
25
|
connectionString: string;
|
|
18
26
|
databaseConnectionString: string;
|
|
19
27
|
database: string;
|
|
20
|
-
stop: () => Promise<void>;
|
|
28
|
+
stop: (options?: StopPgHereOptions) => Promise<void>;
|
|
29
|
+
cleanup: () => Promise<void>;
|
|
21
30
|
ensureDatabase: (databaseName?: string) => Promise<boolean>;
|
|
31
|
+
removeShutdownHooks: () => void;
|
|
22
32
|
}
|
|
23
33
|
export declare function createPgHereInstance(options?: PgHereOptions): PostgresInstance;
|
|
24
34
|
export declare function ensurePgHereDatabase(instance: PostgresInstance, databaseName: string): Promise<boolean>;
|
|
25
|
-
export declare function stopPgHere(instance: PostgresInstance): Promise<void>;
|
|
35
|
+
export declare function stopPgHere(instance: PostgresInstance, options?: StopPgHereOptions): Promise<void>;
|
|
26
36
|
export declare function startPgHere(options?: PgHereOptions): Promise<PgHereHandle>;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import { PostgresInstance } from "pg-embedded";
|
|
3
|
+
import { Client } from "pg";
|
|
3
4
|
const DEFAULT_USERNAME = "postgres";
|
|
4
5
|
const DEFAULT_PASSWORD = "postgres";
|
|
5
6
|
const DEFAULT_PORT = 55432;
|
|
6
7
|
const DEFAULT_DATABASE = "postgres";
|
|
8
|
+
const DEFAULT_SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
9
|
+
const PG_STAT_STATEMENTS_EXTENSION = "pg_stat_statements";
|
|
7
10
|
export function createPgHereInstance(options = {}) {
|
|
8
11
|
const root = resolve(options.projectDir ?? process.cwd());
|
|
9
12
|
const dataDir = resolve(options.dataDir ?? join(root, "pg_local", "data"));
|
|
@@ -30,18 +33,20 @@ export async function ensurePgHereDatabase(instance, databaseName) {
|
|
|
30
33
|
await instance.createDatabase(databaseName);
|
|
31
34
|
return true;
|
|
32
35
|
}
|
|
33
|
-
export async function stopPgHere(instance) {
|
|
36
|
+
export async function stopPgHere(instance, options = {}) {
|
|
34
37
|
try {
|
|
35
38
|
await instance.stop();
|
|
36
39
|
}
|
|
37
40
|
catch {
|
|
38
41
|
// no-op
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
if (options.cleanup) {
|
|
44
|
+
try {
|
|
45
|
+
await instance.cleanup();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// no-op
|
|
49
|
+
}
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
export async function startPgHere(options = {}) {
|
|
@@ -52,15 +57,33 @@ export async function startPgHere(options = {}) {
|
|
|
52
57
|
if (shouldCreate) {
|
|
53
58
|
await ensurePgHereDatabase(instance, database);
|
|
54
59
|
}
|
|
60
|
+
if (options.enablePgStatStatements ?? true) {
|
|
61
|
+
await ensurePgStatStatements(instance, database);
|
|
62
|
+
}
|
|
63
|
+
const defaultCleanupOnShutdown = options.cleanupOnShutdown ?? false;
|
|
55
64
|
const connectionString = instance.connectionInfo.connectionString;
|
|
56
65
|
const databaseConnectionString = setConnectionDatabase(connectionString, database);
|
|
66
|
+
let removeShutdownHooks = () => { };
|
|
67
|
+
const stopHandle = async (stopOptions = {}) => {
|
|
68
|
+
removeShutdownHooks();
|
|
69
|
+
const cleanup = stopOptions.cleanup ?? defaultCleanupOnShutdown;
|
|
70
|
+
await stopPgHere(instance, { cleanup });
|
|
71
|
+
};
|
|
72
|
+
if (options.registerProcessShutdownHandlers ?? true) {
|
|
73
|
+
removeShutdownHooks = registerPgHereShutdownHandlers({
|
|
74
|
+
stop: async () => stopHandle({ cleanup: defaultCleanupOnShutdown }),
|
|
75
|
+
signals: options.shutdownSignals ?? DEFAULT_SHUTDOWN_SIGNALS,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
57
78
|
return {
|
|
58
79
|
instance,
|
|
59
80
|
connectionString,
|
|
60
81
|
databaseConnectionString,
|
|
61
82
|
database,
|
|
62
|
-
stop:
|
|
83
|
+
stop: stopHandle,
|
|
84
|
+
cleanup: async () => stopHandle({ cleanup: true }),
|
|
63
85
|
ensureDatabase: async (databaseName = database) => ensurePgHereDatabase(instance, databaseName),
|
|
86
|
+
removeShutdownHooks,
|
|
64
87
|
};
|
|
65
88
|
}
|
|
66
89
|
function setConnectionDatabase(connectionString, database) {
|
|
@@ -68,3 +91,82 @@ function setConnectionDatabase(connectionString, database) {
|
|
|
68
91
|
connectionUrl.pathname = `/${database}`;
|
|
69
92
|
return connectionUrl.toString();
|
|
70
93
|
}
|
|
94
|
+
async function ensurePgStatStatements(instance, database) {
|
|
95
|
+
await ensureSharedPreloadLibrary(instance, PG_STAT_STATEMENTS_EXTENSION);
|
|
96
|
+
await ensureExtension(instance, database, PG_STAT_STATEMENTS_EXTENSION);
|
|
97
|
+
}
|
|
98
|
+
async function ensureSharedPreloadLibrary(instance, libraryName) {
|
|
99
|
+
const adminConnection = setConnectionDatabase(instance.connectionInfo.connectionString, DEFAULT_DATABASE);
|
|
100
|
+
const client = new Client({ connectionString: adminConnection });
|
|
101
|
+
try {
|
|
102
|
+
await client.connect();
|
|
103
|
+
const result = await client.query("show shared_preload_libraries");
|
|
104
|
+
const rawLibraries = String(result.rows[0]?.shared_preload_libraries ?? "");
|
|
105
|
+
const libraries = parsePreloadLibraries(rawLibraries);
|
|
106
|
+
if (libraries.includes(libraryName)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const nextLibraries = [...libraries, libraryName];
|
|
110
|
+
const librariesValue = escapeSqlLiteral(nextLibraries.join(","));
|
|
111
|
+
await client.query(`ALTER SYSTEM SET shared_preload_libraries = '${librariesValue}'`);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await client.end().catch(() => { });
|
|
115
|
+
}
|
|
116
|
+
await instance.stop().catch(() => { });
|
|
117
|
+
await instance.start();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
async function ensureExtension(instance, database, extensionName) {
|
|
121
|
+
const connectionString = setConnectionDatabase(instance.connectionInfo.connectionString, database);
|
|
122
|
+
const client = new Client({ connectionString });
|
|
123
|
+
try {
|
|
124
|
+
await client.connect();
|
|
125
|
+
await client.query(`CREATE EXTENSION IF NOT EXISTS ${quoteIdentifier(extensionName)}`);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
await client.end().catch(() => { });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function parsePreloadLibraries(value) {
|
|
132
|
+
if (!value) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
return value
|
|
136
|
+
.split(",")
|
|
137
|
+
.map((library) => library.trim())
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
}
|
|
140
|
+
function quoteIdentifier(identifier) {
|
|
141
|
+
return `"${identifier.replaceAll('"', '""')}"`;
|
|
142
|
+
}
|
|
143
|
+
function escapeSqlLiteral(value) {
|
|
144
|
+
return value.replaceAll("'", "''");
|
|
145
|
+
}
|
|
146
|
+
function registerPgHereShutdownHandlers({ stop, signals, }) {
|
|
147
|
+
let isStopping = false;
|
|
148
|
+
const uniqueSignals = [...new Set(signals)];
|
|
149
|
+
const handlers = uniqueSignals.map((signal) => {
|
|
150
|
+
const handler = () => {
|
|
151
|
+
if (isStopping) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
isStopping = true;
|
|
155
|
+
void (async () => {
|
|
156
|
+
try {
|
|
157
|
+
await stop();
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
process.exit(0);
|
|
161
|
+
}
|
|
162
|
+
})();
|
|
163
|
+
};
|
|
164
|
+
process.on(signal, handler);
|
|
165
|
+
return { signal, handler };
|
|
166
|
+
});
|
|
167
|
+
return () => {
|
|
168
|
+
for (const { signal, handler } of handlers) {
|
|
169
|
+
process.off(signal, handler);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { PostgresInstance } from "pg-embedded";
|
|
3
|
+
import { Client } from "pg";
|
|
4
|
+
|
|
5
|
+
export type PgHereShutdownSignal = "SIGINT" | "SIGTERM" | "SIGHUP";
|
|
6
|
+
|
|
7
|
+
export interface PgHereOptions {
|
|
8
|
+
projectDir?: string;
|
|
9
|
+
dataDir?: string;
|
|
10
|
+
installationDir?: string;
|
|
11
|
+
postgresVersion?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
username?: string;
|
|
15
|
+
password?: string;
|
|
16
|
+
persistent?: boolean;
|
|
17
|
+
database?: string;
|
|
18
|
+
createDatabaseIfMissing?: boolean;
|
|
19
|
+
registerProcessShutdownHandlers?: boolean;
|
|
20
|
+
shutdownSignals?: PgHereShutdownSignal[];
|
|
21
|
+
cleanupOnShutdown?: boolean;
|
|
22
|
+
enablePgStatStatements?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StopPgHereOptions {
|
|
26
|
+
cleanup?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PgHereHandle {
|
|
30
|
+
instance: PostgresInstance;
|
|
31
|
+
connectionString: string;
|
|
32
|
+
databaseConnectionString: string;
|
|
33
|
+
database: string;
|
|
34
|
+
stop: (options?: StopPgHereOptions) => Promise<void>;
|
|
35
|
+
cleanup: () => Promise<void>;
|
|
36
|
+
ensureDatabase: (databaseName?: string) => Promise<boolean>;
|
|
37
|
+
removeShutdownHooks: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEFAULT_USERNAME = "postgres";
|
|
41
|
+
const DEFAULT_PASSWORD = "postgres";
|
|
42
|
+
const DEFAULT_PORT = 55432;
|
|
43
|
+
const DEFAULT_DATABASE = "postgres";
|
|
44
|
+
const DEFAULT_SHUTDOWN_SIGNALS: PgHereShutdownSignal[] = ["SIGINT", "SIGTERM"];
|
|
45
|
+
const PG_STAT_STATEMENTS_EXTENSION = "pg_stat_statements";
|
|
46
|
+
|
|
47
|
+
export function createPgHereInstance(options: PgHereOptions = {}): PostgresInstance {
|
|
48
|
+
const root = resolve(options.projectDir ?? process.cwd());
|
|
49
|
+
const dataDir = resolve(options.dataDir ?? join(root, "pg_local", "data"));
|
|
50
|
+
const installationDir = resolve(
|
|
51
|
+
options.installationDir ?? join(root, "pg_local", "bin")
|
|
52
|
+
);
|
|
53
|
+
const postgresVersion = options.postgresVersion ?? options.version;
|
|
54
|
+
|
|
55
|
+
return new PostgresInstance({
|
|
56
|
+
version: postgresVersion,
|
|
57
|
+
dataDir,
|
|
58
|
+
installationDir,
|
|
59
|
+
port: options.port ?? DEFAULT_PORT,
|
|
60
|
+
username: options.username ?? DEFAULT_USERNAME,
|
|
61
|
+
password: options.password ?? DEFAULT_PASSWORD,
|
|
62
|
+
persistent: options.persistent ?? true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function ensurePgHereDatabase(
|
|
67
|
+
instance: PostgresInstance,
|
|
68
|
+
databaseName: string
|
|
69
|
+
): Promise<boolean> {
|
|
70
|
+
if (!databaseName || databaseName === DEFAULT_DATABASE) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const exists = await instance.databaseExists(databaseName);
|
|
75
|
+
if (exists) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await instance.createDatabase(databaseName);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function stopPgHere(
|
|
84
|
+
instance: PostgresInstance,
|
|
85
|
+
options: StopPgHereOptions = {}
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
try {
|
|
88
|
+
await instance.stop();
|
|
89
|
+
} catch {
|
|
90
|
+
// no-op
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (options.cleanup) {
|
|
94
|
+
try {
|
|
95
|
+
await instance.cleanup();
|
|
96
|
+
} catch {
|
|
97
|
+
// no-op
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function startPgHere(options: PgHereOptions = {}): Promise<PgHereHandle> {
|
|
103
|
+
const instance = createPgHereInstance(options);
|
|
104
|
+
await instance.start();
|
|
105
|
+
|
|
106
|
+
const database = options.database ?? DEFAULT_DATABASE;
|
|
107
|
+
const shouldCreate =
|
|
108
|
+
options.createDatabaseIfMissing ?? database !== DEFAULT_DATABASE;
|
|
109
|
+
|
|
110
|
+
if (shouldCreate) {
|
|
111
|
+
await ensurePgHereDatabase(instance, database);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options.enablePgStatStatements ?? true) {
|
|
115
|
+
await ensurePgStatStatements(instance, database);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const defaultCleanupOnShutdown = options.cleanupOnShutdown ?? false;
|
|
119
|
+
const connectionString = instance.connectionInfo.connectionString;
|
|
120
|
+
const databaseConnectionString = setConnectionDatabase(connectionString, database);
|
|
121
|
+
let removeShutdownHooks = () => {};
|
|
122
|
+
|
|
123
|
+
const stopHandle = async (stopOptions: StopPgHereOptions = {}) => {
|
|
124
|
+
removeShutdownHooks();
|
|
125
|
+
const cleanup = stopOptions.cleanup ?? defaultCleanupOnShutdown;
|
|
126
|
+
await stopPgHere(instance, { cleanup });
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (options.registerProcessShutdownHandlers ?? true) {
|
|
130
|
+
removeShutdownHooks = registerPgHereShutdownHandlers({
|
|
131
|
+
stop: async () => stopHandle({ cleanup: defaultCleanupOnShutdown }),
|
|
132
|
+
signals: options.shutdownSignals ?? DEFAULT_SHUTDOWN_SIGNALS,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
instance,
|
|
138
|
+
connectionString,
|
|
139
|
+
databaseConnectionString,
|
|
140
|
+
database,
|
|
141
|
+
stop: stopHandle,
|
|
142
|
+
cleanup: async () => stopHandle({ cleanup: true }),
|
|
143
|
+
ensureDatabase: async (databaseName = database) =>
|
|
144
|
+
ensurePgHereDatabase(instance, databaseName),
|
|
145
|
+
removeShutdownHooks,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function setConnectionDatabase(connectionString: string, database: string): string {
|
|
150
|
+
const connectionUrl = new URL(connectionString);
|
|
151
|
+
connectionUrl.pathname = `/${database}`;
|
|
152
|
+
return connectionUrl.toString();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function ensurePgStatStatements(
|
|
156
|
+
instance: PostgresInstance,
|
|
157
|
+
database: string
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
await ensureSharedPreloadLibrary(instance, PG_STAT_STATEMENTS_EXTENSION);
|
|
160
|
+
await ensureExtension(instance, database, PG_STAT_STATEMENTS_EXTENSION);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function ensureSharedPreloadLibrary(
|
|
164
|
+
instance: PostgresInstance,
|
|
165
|
+
libraryName: string
|
|
166
|
+
): Promise<boolean> {
|
|
167
|
+
const adminConnection = setConnectionDatabase(
|
|
168
|
+
instance.connectionInfo.connectionString,
|
|
169
|
+
DEFAULT_DATABASE
|
|
170
|
+
);
|
|
171
|
+
const client = new Client({ connectionString: adminConnection });
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await client.connect();
|
|
175
|
+
const result = await client.query("show shared_preload_libraries");
|
|
176
|
+
const rawLibraries = String(result.rows[0]?.shared_preload_libraries ?? "");
|
|
177
|
+
const libraries = parsePreloadLibraries(rawLibraries);
|
|
178
|
+
|
|
179
|
+
if (libraries.includes(libraryName)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const nextLibraries = [...libraries, libraryName];
|
|
184
|
+
const librariesValue = escapeSqlLiteral(nextLibraries.join(","));
|
|
185
|
+
await client.query(
|
|
186
|
+
`ALTER SYSTEM SET shared_preload_libraries = '${librariesValue}'`
|
|
187
|
+
);
|
|
188
|
+
} finally {
|
|
189
|
+
await client.end().catch(() => {});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await instance.stop().catch(() => {});
|
|
193
|
+
await instance.start();
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function ensureExtension(
|
|
198
|
+
instance: PostgresInstance,
|
|
199
|
+
database: string,
|
|
200
|
+
extensionName: string
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
const connectionString = setConnectionDatabase(
|
|
203
|
+
instance.connectionInfo.connectionString,
|
|
204
|
+
database
|
|
205
|
+
);
|
|
206
|
+
const client = new Client({ connectionString });
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await client.connect();
|
|
210
|
+
await client.query(`CREATE EXTENSION IF NOT EXISTS ${quoteIdentifier(extensionName)}`);
|
|
211
|
+
} finally {
|
|
212
|
+
await client.end().catch(() => {});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parsePreloadLibraries(value: string): string[] {
|
|
217
|
+
if (!value) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return value
|
|
222
|
+
.split(",")
|
|
223
|
+
.map((library) => library.trim())
|
|
224
|
+
.filter(Boolean);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function quoteIdentifier(identifier: string): string {
|
|
228
|
+
return `"${identifier.replaceAll('"', '""')}"`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function escapeSqlLiteral(value: string): string {
|
|
232
|
+
return value.replaceAll("'", "''");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function registerPgHereShutdownHandlers({
|
|
236
|
+
stop,
|
|
237
|
+
signals,
|
|
238
|
+
}: {
|
|
239
|
+
stop: () => Promise<void>;
|
|
240
|
+
signals: PgHereShutdownSignal[];
|
|
241
|
+
}): () => void {
|
|
242
|
+
let isStopping = false;
|
|
243
|
+
const uniqueSignals = [...new Set(signals)];
|
|
244
|
+
|
|
245
|
+
const handlers = uniqueSignals.map((signal) => {
|
|
246
|
+
const handler = () => {
|
|
247
|
+
if (isStopping) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
isStopping = true;
|
|
251
|
+
|
|
252
|
+
void (async () => {
|
|
253
|
+
try {
|
|
254
|
+
await stop();
|
|
255
|
+
} finally {
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
})();
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
process.on(signal, handler);
|
|
262
|
+
return { signal, handler };
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return () => {
|
|
266
|
+
for (const { signal, handler } of handlers) {
|
|
267
|
+
process.off(signal, handler);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pg-here",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Per-project embedded PostgreSQL with programmatic startup and auto DB creation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"pg-here": "scripts/pg-dev.mjs"
|
|
10
|
+
},
|
|
8
11
|
"exports": {
|
|
9
12
|
".": {
|
|
10
13
|
"types": "./dist/index.d.ts",
|
|
@@ -13,6 +16,8 @@
|
|
|
13
16
|
},
|
|
14
17
|
"files": [
|
|
15
18
|
"dist",
|
|
19
|
+
"index.ts",
|
|
20
|
+
"scripts/pg-dev.mjs",
|
|
16
21
|
"README.md"
|
|
17
22
|
],
|
|
18
23
|
"private": false,
|
|
@@ -49,6 +54,7 @@
|
|
|
49
54
|
},
|
|
50
55
|
"devDependencies": {
|
|
51
56
|
"@types/bun": "latest",
|
|
57
|
+
"@types/pg": "^8.15.6",
|
|
52
58
|
"typescript": "^5.9.2"
|
|
53
59
|
},
|
|
54
60
|
"dependencies": {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import yargs from "yargs";
|
|
2
|
+
import { hideBin } from "yargs/helpers";
|
|
3
|
+
import { startPgHere } from "../index.ts";
|
|
4
|
+
|
|
5
|
+
const argv = await yargs(hideBin(process.argv))
|
|
6
|
+
.version(false)
|
|
7
|
+
.option("username", {
|
|
8
|
+
alias: "u",
|
|
9
|
+
default: "postgres",
|
|
10
|
+
describe: "PostgreSQL username",
|
|
11
|
+
})
|
|
12
|
+
.option("password", {
|
|
13
|
+
alias: "p",
|
|
14
|
+
default: "postgres",
|
|
15
|
+
describe: "PostgreSQL password",
|
|
16
|
+
})
|
|
17
|
+
.option("port", {
|
|
18
|
+
default: 55432,
|
|
19
|
+
describe: "PostgreSQL port",
|
|
20
|
+
})
|
|
21
|
+
.option("database", {
|
|
22
|
+
alias: "d",
|
|
23
|
+
default: "postgres",
|
|
24
|
+
describe: "Database to use (created automatically if missing)",
|
|
25
|
+
})
|
|
26
|
+
.option("pg-version", {
|
|
27
|
+
default: process.env.PG_VERSION,
|
|
28
|
+
describe: "PostgreSQL version (e.g. 18.0.0 or >=17.0)",
|
|
29
|
+
})
|
|
30
|
+
.parse();
|
|
31
|
+
|
|
32
|
+
const pg = await startPgHere({
|
|
33
|
+
projectDir: process.cwd(),
|
|
34
|
+
port: argv.port,
|
|
35
|
+
username: argv.username,
|
|
36
|
+
password: argv.password,
|
|
37
|
+
database: argv.database,
|
|
38
|
+
postgresVersion: argv["pg-version"],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// print connection string for tooling
|
|
42
|
+
console.log(pg.databaseConnectionString);
|
|
43
|
+
|
|
44
|
+
// keep this process alive; Ctrl-C stops postgres
|
|
45
|
+
setInterval(() => {}, 1 << 30);
|