pg-here 0.1.2 → 0.1.4

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 fails with `could not determine executable to run for package`, install at least `pg-here@0.1.4` (this is the first release with a real package binary).
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.
@@ -57,16 +83,16 @@ Set `enablePgStatStatements: false` to opt out.
57
83
 
58
84
  ```bash
59
85
  # Custom credentials
60
- bun run db:up --username myuser --password mypass
86
+ bun run db:up --username postgres --password postgres
61
87
 
62
88
  # Short flags
63
- bun run db:up -u myuser -p mypass
89
+ bun run db:up -u postgres -p postgres
64
90
 
65
91
  # Custom port
66
92
  bun run db:up --port 55433
67
93
 
68
94
  # All together
69
- bun run db:up -u myuser -p mypass --port 55433
95
+ bun run db:up -u postgres -p postgres --database postgres --port 55433
70
96
 
71
97
  # Pin postgres version
72
98
  bun run db:up --pg-version 18.0.0
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ import yargs from "yargs";
4
+ import { hideBin } from "yargs/helpers";
5
+ import { startPgHere } from "../dist/index.js";
6
+
7
+ const argv = await yargs(hideBin(process.argv))
8
+ .version(false)
9
+ .option("username", {
10
+ alias: "u",
11
+ default: "postgres",
12
+ describe: "PostgreSQL username",
13
+ })
14
+ .option("password", {
15
+ alias: "p",
16
+ default: "postgres",
17
+ describe: "PostgreSQL password",
18
+ })
19
+ .option("port", {
20
+ default: 55432,
21
+ describe: "PostgreSQL port",
22
+ })
23
+ .option("database", {
24
+ alias: "d",
25
+ default: "postgres",
26
+ describe: "Database to use (created automatically if missing)",
27
+ })
28
+ .option("pg-version", {
29
+ default: process.env.PG_VERSION,
30
+ describe: "PostgreSQL version (e.g. 18.0.0 or >=17.0)",
31
+ })
32
+ .parse();
33
+
34
+ const pg = await startPgHere({
35
+ projectDir: process.cwd(),
36
+ port: argv.port,
37
+ username: argv.username,
38
+ password: argv.password,
39
+ database: argv.database,
40
+ postgresVersion: argv["pg-version"],
41
+ });
42
+
43
+ console.log(pg.databaseConnectionString);
44
+ setInterval(() => {}, 1 << 30);
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.2",
3
+ "version": "0.1.4",
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": "bin/pg-here.mjs"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "types": "./dist/index.d.ts",
@@ -13,6 +16,9 @@
13
16
  },
14
17
  "files": [
15
18
  "dist",
19
+ "index.ts",
20
+ "bin/pg-here.mjs",
21
+ "scripts/pg-dev.mjs",
16
22
  "README.md"
17
23
  ],
18
24
  "private": false,
@@ -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);