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 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 myuser --password mypass
86
+ bun run db:up --username postgres --password postgres
56
87
 
57
88
  # Short flags
58
- bun run db:up -u myuser -p mypass
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 myuser -p mypass --port 55433
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
- try {
41
- await instance.cleanup();
42
- }
43
- catch {
44
- // no-op
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: async () => stopPgHere(instance),
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.0",
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);