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 +29 -3
- package/bin/pg-here.mjs +44 -0
- 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 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
|
|
86
|
+
bun run db:up --username postgres --password postgres
|
|
61
87
|
|
|
62
88
|
# Short flags
|
|
63
|
-
bun run db:up -u
|
|
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
|
|
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
|
package/bin/pg-here.mjs
ADDED
|
@@ -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.
|
|
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);
|