@owlmeans/postgres 0.1.15
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/LICENSE +21 -0
- package/README.md +139 -0
- package/agent-meta/instructions/postgres.instructions.md +57 -0
- package/agent-meta/manifest.json +23 -0
- package/agent-meta/skills/postgres/SKILL.md +121 -0
- package/build/bootstrap.d.ts +16 -0
- package/build/bootstrap.d.ts.map +1 -0
- package/build/bootstrap.js +106 -0
- package/build/bootstrap.js.map +1 -0
- package/build/consts.d.ts +29 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +30 -0
- package/build/consts.js.map +1 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/middleware.d.ts +11 -0
- package/build/middleware.d.ts.map +1 -0
- package/build/middleware.js +21 -0
- package/build/middleware.js.map +1 -0
- package/build/service.d.ts +9 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +221 -0
- package/build/service.js.map +1 -0
- package/build/types.d.ts +40 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/config.d.ts +27 -0
- package/build/utils/config.d.ts.map +1 -0
- package/build/utils/config.js +96 -0
- package/build/utils/config.js.map +1 -0
- package/build/utils/connection.d.ts +15 -0
- package/build/utils/connection.d.ts.map +1 -0
- package/build/utils/connection.js +56 -0
- package/build/utils/connection.js.map +1 -0
- package/package.json +45 -0
- package/src/bootstrap.ts +126 -0
- package/src/consts.ts +36 -0
- package/src/index.ts +7 -0
- package/src/middleware.ts +26 -0
- package/src/service.ts +287 -0
- package/src/types.ts +42 -0
- package/src/utils/config.ts +109 -0
- package/src/utils/connection.ts +64 -0
- package/tests/bootstrap.spec.ts +142 -0
- package/tests/context.ts +204 -0
- package/tests/crud.spec.ts +227 -0
- package/tests/custom-sql.spec.ts +196 -0
- package/tests/migration.spec.ts +260 -0
- package/tests/sync.spec.ts +156 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAS3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAIjD,KAAK,MAAM,GAAG,YAAY,CAAA;AAC1B,UAAU,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;CAAI;AAEzE,eAAO,MAAM,qBAAqB,GAAI,QAAO,MAAsB,KAAG,eA2PrE,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAChF,SAAS,CAAC,EAAE,QAAO,MAAsB,KACxC,CAOF,CAAA"}
|
package/build/service.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { makeKeyPairModel } from '@owlmeans/basic-keys';
|
|
2
|
+
import { assertContext, Layer } from '@owlmeans/context';
|
|
3
|
+
import { makeTx, pgErrorToResourceError, pgIdentifier, refOf, resolvePlaceholders } from '@owlmeans/postgres-resource';
|
|
4
|
+
import { createDbService } from '@owlmeans/resource';
|
|
5
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
6
|
+
import { Pool } from 'pg';
|
|
7
|
+
import { DEFAULT_ALIAS } from './consts.js';
|
|
8
|
+
import { bootstrapDb } from './bootstrap.js';
|
|
9
|
+
import { drainMiddleware } from './middleware.js';
|
|
10
|
+
import { poolDatabase, prepareConfig } from './utils/config.js';
|
|
11
|
+
import { ensureSchema, probe } from './utils/connection.js';
|
|
12
|
+
export const makePostgresDbService = (alias = DEFAULT_ALIAS) => {
|
|
13
|
+
const location = `postgres:${alias}`;
|
|
14
|
+
/**
|
|
15
|
+
* Handles are cached per config alias rather than rebuilt per call: `db()` runs on every
|
|
16
|
+
* single resource operation, and both the Drizzle binding and the schema name are fixed
|
|
17
|
+
* for the lifetime of a service instance — a layer switch produces a *new* instance.
|
|
18
|
+
*/
|
|
19
|
+
const handles = new Map();
|
|
20
|
+
const deferred = new Map();
|
|
21
|
+
const service = createDbService(alias, {
|
|
22
|
+
db: async (configAlias) => {
|
|
23
|
+
const pool = await service.client(configAlias);
|
|
24
|
+
configAlias = service.ensureConfigAlias(configAlias);
|
|
25
|
+
let handle = handles.get(configAlias);
|
|
26
|
+
if (handle == null) {
|
|
27
|
+
handle = {
|
|
28
|
+
drizzle: drizzle(pool),
|
|
29
|
+
pool,
|
|
30
|
+
/**
|
|
31
|
+
* `name()` is the layer aware namespace — the Postgres SCHEMA here, where Mongo
|
|
32
|
+
* reads the same value as a database name. It can outgrow 63 bytes once a layer
|
|
33
|
+
* suffix is appended, which Postgres would truncate silently.
|
|
34
|
+
*/
|
|
35
|
+
schema: pgIdentifier(service.name(configAlias)),
|
|
36
|
+
database: poolDatabase(pool)
|
|
37
|
+
};
|
|
38
|
+
handles.set(configAlias, handle);
|
|
39
|
+
}
|
|
40
|
+
return handle;
|
|
41
|
+
},
|
|
42
|
+
initialize: async (configAlias) => {
|
|
43
|
+
configAlias = service.ensureConfigAlias(configAlias);
|
|
44
|
+
const config = service.config(configAlias);
|
|
45
|
+
if (service.clients[configAlias] != null) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (service.layers == null) {
|
|
49
|
+
service.layers = [Layer.Global];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Sensitivity *adds* a layer the service is willing to serve. The Mongo copy of this
|
|
53
|
+
* block tests `includes` where it means `!includes`, so it only ever appends a layer
|
|
54
|
+
* that is already there — a latent no-op, corrected here.
|
|
55
|
+
*/
|
|
56
|
+
if (config.serviceSensitive === true && !service.layers.includes(Layer.Service)) {
|
|
57
|
+
service.layers.push(Layer.Service);
|
|
58
|
+
}
|
|
59
|
+
if (config.entitySensitive === true && !service.layers.includes(Layer.Entity)) {
|
|
60
|
+
service.layers.push(Layer.Entity);
|
|
61
|
+
}
|
|
62
|
+
const meta = (config.meta ?? {});
|
|
63
|
+
const pool = new Pool(prepareConfig(config));
|
|
64
|
+
/**
|
|
65
|
+
* Not optional: node-postgres emits `error` on *idle* clients when the server closes
|
|
66
|
+
* a connection — a routine event behind a load balancer — and an unhandled `error`
|
|
67
|
+
* on an EventEmitter terminates the process.
|
|
68
|
+
*/
|
|
69
|
+
pool.on('error', error => {
|
|
70
|
+
console.error(`${location}: idle client error — ${error.message}`);
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await probe(pool, meta, location);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
await pool.end().catch(() => undefined);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
process.on('SIGTERM', () => { void pool.end().catch(() => undefined); });
|
|
80
|
+
if (service.clients[configAlias] != null) {
|
|
81
|
+
await pool.end().catch(() => undefined);
|
|
82
|
+
throw new SyntaxError(`Cannot replace existing postgres client: ${configAlias} - ${service.alias}`);
|
|
83
|
+
}
|
|
84
|
+
service.clients[configAlias] = pool;
|
|
85
|
+
/**
|
|
86
|
+
* Only when the config names one. Resources create their own schema anyway, and the
|
|
87
|
+
* admin connection points at the maintenance database — where inventing a namespace
|
|
88
|
+
* from the service alias would leave an empty `postgres` schema behind on every boot.
|
|
89
|
+
*/
|
|
90
|
+
if (config.schema != null) {
|
|
91
|
+
await ensureSchema(pool, pgIdentifier(service.name(configAlias)));
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* `configAlias` is part of the contract for symmetry only — a resource's qualified
|
|
96
|
+
* name comes from the table spec it compiled at init, which already resolved against
|
|
97
|
+
* whichever config that resource was bound to.
|
|
98
|
+
*/
|
|
99
|
+
qualify: resourceAlias => {
|
|
100
|
+
const context = assertContext(service.ctx, location);
|
|
101
|
+
return refOf(context, null, resourceAlias);
|
|
102
|
+
},
|
|
103
|
+
query: async (text, params, configAlias) => {
|
|
104
|
+
const db = await service.db(configAlias);
|
|
105
|
+
const context = assertContext(service.ctx, location);
|
|
106
|
+
try {
|
|
107
|
+
const result = await db.pool.query(resolvePlaceholders(text, context, null), params);
|
|
108
|
+
return result.rows;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw pgErrorToResourceError(error);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
transaction: async (fn, configAlias) => {
|
|
115
|
+
const db = await service.db(configAlias);
|
|
116
|
+
const context = assertContext(service.ctx, location);
|
|
117
|
+
const client = await db.pool.connect();
|
|
118
|
+
try {
|
|
119
|
+
await client.query('BEGIN');
|
|
120
|
+
const result = await fn(makeTx(client, text => resolvePlaceholders(text, context, null), resourceAlias => refOf(context, null, resourceAlias)));
|
|
121
|
+
await client.query('COMMIT');
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
126
|
+
throw pgErrorToResourceError(error);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
client.release();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
defer: (configAlias, task) => {
|
|
133
|
+
configAlias = service.ensureConfigAlias(configAlias);
|
|
134
|
+
const tasks = deferred.get(configAlias);
|
|
135
|
+
if (tasks == null) {
|
|
136
|
+
deferred.set(configAlias, [task]);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
tasks.push(task);
|
|
140
|
+
},
|
|
141
|
+
drain: async (configAlias) => {
|
|
142
|
+
const aliases = configAlias != null
|
|
143
|
+
? [service.ensureConfigAlias(configAlias)]
|
|
144
|
+
: [...deferred.keys()];
|
|
145
|
+
for (const key of aliases) {
|
|
146
|
+
const tasks = deferred.get(key);
|
|
147
|
+
if (tasks == null) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Cleared before running, not after: a task that throws has still had its side
|
|
152
|
+
* effects, and a later drain replaying it would apply them twice.
|
|
153
|
+
*/
|
|
154
|
+
deferred.delete(key);
|
|
155
|
+
for (const task of tasks) {
|
|
156
|
+
await task();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
bootstrap: async (configAlias, opts) => bootstrapDb(service, configAlias, opts),
|
|
161
|
+
lock: async (configAlias, record, fields) => {
|
|
162
|
+
configAlias = service.ensureConfigAlias(configAlias);
|
|
163
|
+
const config = service.config(configAlias);
|
|
164
|
+
if (config.encryptionKey == null) {
|
|
165
|
+
throw new SyntaxError(`No encryption key for locking: ${configAlias}`);
|
|
166
|
+
}
|
|
167
|
+
if ((fields.length ?? 0) < 1) {
|
|
168
|
+
throw new SyntaxError(`No fields to lock: ${JSON.stringify(record)}`);
|
|
169
|
+
}
|
|
170
|
+
const key = makeKeyPairModel(config.encryptionKey);
|
|
171
|
+
return Object.fromEntries(await Promise.all(Object.entries(record).map(async ([field, value]) => [
|
|
172
|
+
field,
|
|
173
|
+
fields.includes(field) ? await key.encrypt(value) : value
|
|
174
|
+
])));
|
|
175
|
+
},
|
|
176
|
+
unlock: async (configAlias, record, fields) => {
|
|
177
|
+
configAlias = service.ensureConfigAlias(configAlias);
|
|
178
|
+
const config = service.config(configAlias);
|
|
179
|
+
if (config.encryptionKey == null) {
|
|
180
|
+
throw new SyntaxError(`No encryption key for unlocking: ${configAlias}`);
|
|
181
|
+
}
|
|
182
|
+
if ((fields.length ?? 0) < 1) {
|
|
183
|
+
throw new SyntaxError(`No fields to unlock: ${JSON.stringify(record)}`);
|
|
184
|
+
}
|
|
185
|
+
const key = makeKeyPairModel(config.encryptionKey);
|
|
186
|
+
return Object.fromEntries(await Promise.all(Object.entries(record).map(async ([field, value]) => [
|
|
187
|
+
field,
|
|
188
|
+
/** A value that was never locked decrypts to itself rather than failing. */
|
|
189
|
+
fields.includes(field) ? await key.decrypt(value).catch(() => value) : value
|
|
190
|
+
])));
|
|
191
|
+
},
|
|
192
|
+
reinitializeContext: (context) => {
|
|
193
|
+
const _service = makePostgresDbService(alias);
|
|
194
|
+
_service.ctx = context;
|
|
195
|
+
_service.layers = service.layers;
|
|
196
|
+
/**
|
|
197
|
+
* Pools carry over instead of being reopened. A layer switch changes the Postgres
|
|
198
|
+
* *schema*, not the server, and `max_connections` is a cluster wide budget — a pool
|
|
199
|
+
* per tenant exhausts it long before the tenants run out. The handle cache is
|
|
200
|
+
* deliberately *not* carried: its schema name is what the switch just changed.
|
|
201
|
+
*/
|
|
202
|
+
Object.assign(_service.clients, service.clients);
|
|
203
|
+
return _service;
|
|
204
|
+
}
|
|
205
|
+
}, service => async () => {
|
|
206
|
+
const context = assertContext(service.ctx, location);
|
|
207
|
+
await context.cfg.dbs?.filter(dbConfig => dbConfig.service === alias).reduce(async (prev, dbConfig) => {
|
|
208
|
+
await prev;
|
|
209
|
+
service.config(dbConfig.alias);
|
|
210
|
+
}, Promise.resolve());
|
|
211
|
+
service.initialized = true;
|
|
212
|
+
});
|
|
213
|
+
return service;
|
|
214
|
+
};
|
|
215
|
+
export const appendPostgres = (context, alias = DEFAULT_ALIAS) => {
|
|
216
|
+
const service = makePostgresDbService(alias);
|
|
217
|
+
context.registerService(service);
|
|
218
|
+
context.registerMiddleware(drainMiddleware(alias));
|
|
219
|
+
return context;
|
|
220
|
+
};
|
|
221
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PostgresDbService } from '@owlmeans/postgres-resource';
|
|
2
|
+
export interface PostgresService extends PostgresDbService {
|
|
3
|
+
/**
|
|
4
|
+
* Provision a least privileged role, its database and its schema, using a superuser
|
|
5
|
+
* connection held under a *separate* config alias.
|
|
6
|
+
*
|
|
7
|
+
* Idempotent by design — every OwlMeans deployment calls it on each start and each
|
|
8
|
+
* rebuild, so it probes before it creates and never fails on "already exists".
|
|
9
|
+
*
|
|
10
|
+
* @throws {PostgresBootstrapError}
|
|
11
|
+
*/
|
|
12
|
+
bootstrap: (configAlias: string, opts: BootstrapOptions) => Promise<BootstrapReport>;
|
|
13
|
+
}
|
|
14
|
+
export interface BootstrapOptions {
|
|
15
|
+
/** Login role to create, or whose password to rotate. */
|
|
16
|
+
role: string;
|
|
17
|
+
password: string;
|
|
18
|
+
/** Database to create, owned by `role`. Defaults to the role name. */
|
|
19
|
+
database?: string;
|
|
20
|
+
/** Schema to create inside it, owned by `role` and set as its `search_path`. */
|
|
21
|
+
schema?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Revoke `PUBLIC`'s rights on the database and on `public`, granting them to `role`
|
|
24
|
+
* alone. Defaults to `true` — the point of the admin path is least privilege.
|
|
25
|
+
*/
|
|
26
|
+
leastPrivilege?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Reset the password of a role that already exists. Defaults to `true`, because the
|
|
29
|
+
* caller generated the password it just passed in and the role has to accept it.
|
|
30
|
+
*/
|
|
31
|
+
rotatePassword?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface BootstrapReport {
|
|
34
|
+
roleCreated: boolean;
|
|
35
|
+
passwordRotated: boolean;
|
|
36
|
+
databaseCreated: boolean;
|
|
37
|
+
schemaCreated: boolean;
|
|
38
|
+
grantsApplied: boolean;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAEpE,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD;;;;;;;;OAQG;IACH,SAAS,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;CACrF;AAED,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,OAAO,CAAA;IACpB,eAAe,EAAE,OAAO,CAAA;IACxB,eAAe,EAAE,OAAO,CAAA;IACxB,aAAa,EAAE,OAAO,CAAA;IACtB,aAAa,EAAE,OAAO,CAAA;CACvB"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DbConfig } from '@owlmeans/resource';
|
|
2
|
+
import type { PoolConfig } from 'pg';
|
|
3
|
+
/**
|
|
4
|
+
* Split a `postgres://` URL into pool fields.
|
|
5
|
+
*
|
|
6
|
+
* node-postgres accepts a `connectionString` directly, but the parsed values *override*
|
|
7
|
+
* every sibling field — so `{ connectionString, database }` silently ignores the
|
|
8
|
+
* database. Parsing here instead keeps overriding possible, which the admin path needs
|
|
9
|
+
* to reach both the maintenance database and the target one over the same credentials.
|
|
10
|
+
*
|
|
11
|
+
* @throws {PostgresConnectionError}
|
|
12
|
+
*/
|
|
13
|
+
export declare const parseUrl: (url: string) => PoolConfig;
|
|
14
|
+
/**
|
|
15
|
+
* Turn a {@link DbConfig} into a `pg` pool configuration.
|
|
16
|
+
*
|
|
17
|
+
* Values that start with `/` have already been read from disk by the `fileConfigReader`
|
|
18
|
+
* middleware, so a file mounted secret and a literal one are indistinguishable here —
|
|
19
|
+
* which is what lets a deployment move from env vars to mounted files without a code
|
|
20
|
+
* change.
|
|
21
|
+
*/
|
|
22
|
+
export declare const prepareConfig: (config: DbConfig, overrides?: PoolConfig) => PoolConfig;
|
|
23
|
+
/** Effective database of an open pool, as `pg` resolved it. */
|
|
24
|
+
export declare const poolDatabase: (pool: {
|
|
25
|
+
options?: PoolConfig;
|
|
26
|
+
}) => string;
|
|
27
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AAIpC;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,MAAM,KAAG,UAsCtC,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,QAAQ,EAAE,YAAY,UAAU,KAAG,UAuCxE,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,YAAY,GAAI,MAAM;IAAE,OAAO,CAAC,EAAE,UAAU,CAAA;CAAE,KAAG,MACV,CAAA"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { PostgresConnectionError } from '@owlmeans/postgres-resource';
|
|
2
|
+
import { DEF_POOL_SIZE, DEF_PORT } from '../consts.js';
|
|
3
|
+
/**
|
|
4
|
+
* Split a `postgres://` URL into pool fields.
|
|
5
|
+
*
|
|
6
|
+
* node-postgres accepts a `connectionString` directly, but the parsed values *override*
|
|
7
|
+
* every sibling field — so `{ connectionString, database }` silently ignores the
|
|
8
|
+
* database. Parsing here instead keeps overriding possible, which the admin path needs
|
|
9
|
+
* to reach both the maintenance database and the target one over the same credentials.
|
|
10
|
+
*
|
|
11
|
+
* @throws {PostgresConnectionError}
|
|
12
|
+
*/
|
|
13
|
+
export const parseUrl = (url) => {
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = new URL(url);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new PostgresConnectionError('malformed-url');
|
|
20
|
+
}
|
|
21
|
+
if (!/^postgres(ql)?:$/.test(parsed.protocol)) {
|
|
22
|
+
throw new PostgresConnectionError(`unexpected-protocol:${parsed.protocol.replace(':', '')}`);
|
|
23
|
+
}
|
|
24
|
+
const database = decodeURIComponent(parsed.pathname.replace(/^\//, ''));
|
|
25
|
+
/** IPv6 literals arrive bracketed from `URL`; `pg` wants the bare address. */
|
|
26
|
+
const host = decodeURIComponent(parsed.hostname).replace(/^\[|\]$/g, '');
|
|
27
|
+
const mode = parsed.searchParams.get('sslmode');
|
|
28
|
+
const config = {
|
|
29
|
+
host,
|
|
30
|
+
port: parsed.port !== '' ? parseInt(parsed.port, 10) : DEF_PORT
|
|
31
|
+
};
|
|
32
|
+
if (parsed.username !== '') {
|
|
33
|
+
config.user = decodeURIComponent(parsed.username);
|
|
34
|
+
}
|
|
35
|
+
if (parsed.password !== '') {
|
|
36
|
+
config.password = decodeURIComponent(parsed.password);
|
|
37
|
+
}
|
|
38
|
+
if (database !== '') {
|
|
39
|
+
config.database = database;
|
|
40
|
+
}
|
|
41
|
+
if (mode != null && mode !== 'disable') {
|
|
42
|
+
/**
|
|
43
|
+
* `no-verify` is what a self signed in-cluster certificate needs, and is the only
|
|
44
|
+
* mode that may skip verification — anything else keeps the chain checked.
|
|
45
|
+
*/
|
|
46
|
+
config.ssl = mode === 'no-verify' ? { rejectUnauthorized: false } : true;
|
|
47
|
+
}
|
|
48
|
+
return config;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Turn a {@link DbConfig} into a `pg` pool configuration.
|
|
52
|
+
*
|
|
53
|
+
* Values that start with `/` have already been read from disk by the `fileConfigReader`
|
|
54
|
+
* middleware, so a file mounted secret and a literal one are indistinguishable here —
|
|
55
|
+
* which is what lets a deployment move from env vars to mounted files without a code
|
|
56
|
+
* change.
|
|
57
|
+
*/
|
|
58
|
+
export const prepareConfig = (config, overrides) => {
|
|
59
|
+
const meta = (config.meta ?? {});
|
|
60
|
+
/**
|
|
61
|
+
* `DbConfig.host` is an array for Mongo's replica sets. Postgres has no client side
|
|
62
|
+
* equivalent — multi-host failover is the pooler's job — so only the first is used.
|
|
63
|
+
*/
|
|
64
|
+
const host = Array.isArray(config.host) ? config.host[0] : config.host;
|
|
65
|
+
const base = meta.url != null
|
|
66
|
+
? parseUrl(meta.url)
|
|
67
|
+
: {
|
|
68
|
+
host,
|
|
69
|
+
port: config.port ?? DEF_PORT,
|
|
70
|
+
...(config.user != null ? { user: config.user } : {}),
|
|
71
|
+
...(config.secret != null ? { password: config.secret } : {}),
|
|
72
|
+
/** Absent, libpq falls back to the connection user's name. */
|
|
73
|
+
...(meta.database != null ? { database: meta.database } : {})
|
|
74
|
+
};
|
|
75
|
+
const prepared = {
|
|
76
|
+
...base,
|
|
77
|
+
max: meta.max ?? DEF_POOL_SIZE,
|
|
78
|
+
...(meta.ssl != null ? { ssl: meta.ssl } : {}),
|
|
79
|
+
...(meta.idleTimeoutMillis != null ? { idleTimeoutMillis: meta.idleTimeoutMillis } : {}),
|
|
80
|
+
...(meta.connectionTimeoutMillis != null
|
|
81
|
+
? { connectionTimeoutMillis: meta.connectionTimeoutMillis }
|
|
82
|
+
: {}),
|
|
83
|
+
/**
|
|
84
|
+
* A server side cap, so a query that outlives its caller stops holding a connection
|
|
85
|
+
* and its locks — the failure mode that turns one slow statement into a stuck pool.
|
|
86
|
+
*/
|
|
87
|
+
...(meta.statementTimeoutMillis != null
|
|
88
|
+
? { statement_timeout: meta.statementTimeoutMillis }
|
|
89
|
+
: {}),
|
|
90
|
+
...overrides
|
|
91
|
+
};
|
|
92
|
+
return prepared;
|
|
93
|
+
};
|
|
94
|
+
/** Effective database of an open pool, as `pg` resolved it. */
|
|
95
|
+
export const poolDatabase = (pool) => pool.options?.database ?? pool.options?.user ?? '';
|
|
96
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PostgresMeta } from '@owlmeans/postgres-resource';
|
|
2
|
+
import type { Pool } from 'pg';
|
|
3
|
+
/**
|
|
4
|
+
* Wait for the server to answer a query, not merely to accept a socket.
|
|
5
|
+
*
|
|
6
|
+
* Postgres binds its port before it finishes recovery, so a sidecar or an operator managed
|
|
7
|
+
* instance routinely accepts a connection and then refuses to run anything. Every OwlMeans
|
|
8
|
+
* deployment grew its own copy of this loop; this is the one.
|
|
9
|
+
*
|
|
10
|
+
* @throws {PostgresConnectionError}
|
|
11
|
+
*/
|
|
12
|
+
export declare const probe: (pool: Pool, meta: PostgresMeta, location: string) => Promise<void>;
|
|
13
|
+
/** `CREATE SCHEMA IF NOT EXISTS` on a pooled connection. */
|
|
14
|
+
export declare const ensureSchema: (pool: Pool, schema: string) => Promise<void>;
|
|
15
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/utils/connection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAA;AAkB9B;;;;;;;;GAQG;AACH,eAAO,MAAM,KAAK,GAAU,MAAM,IAAI,EAAE,MAAM,YAAY,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,IAAI,CAyB1F,CAAA;AAED,4DAA4D;AAC5D,eAAO,MAAM,YAAY,GAAU,MAAM,IAAI,EAAE,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAM3E,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { pgErrorToResourceError, PostgresConnectionError, quoteIdent } from '@owlmeans/postgres-resource';
|
|
2
|
+
import { DEF_RETRIES, DEF_RETRY_DELAY, TERMINAL_CONNECT_CODES } from '../consts.js';
|
|
3
|
+
/**
|
|
4
|
+
* Retryable means "the server isn't up yet". A driver level failure carries no `code` at
|
|
5
|
+
* all (`ECONNREFUSED`, DNS) and is exactly the case worth waiting on; a Postgres error
|
|
6
|
+
* code that names a credential or catalog problem will say the same thing in a minute.
|
|
7
|
+
*/
|
|
8
|
+
const isTransient = (error) => {
|
|
9
|
+
const code = error?.code;
|
|
10
|
+
if (code == null) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
return !TERMINAL_CONNECT_CODES.includes(code);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Wait for the server to answer a query, not merely to accept a socket.
|
|
17
|
+
*
|
|
18
|
+
* Postgres binds its port before it finishes recovery, so a sidecar or an operator managed
|
|
19
|
+
* instance routinely accepts a connection and then refuses to run anything. Every OwlMeans
|
|
20
|
+
* deployment grew its own copy of this loop; this is the one.
|
|
21
|
+
*
|
|
22
|
+
* @throws {PostgresConnectionError}
|
|
23
|
+
*/
|
|
24
|
+
export const probe = async (pool, meta, location) => {
|
|
25
|
+
const retries = Math.max(1, meta.retries ?? DEF_RETRIES);
|
|
26
|
+
const delay = meta.retryDelayMillis ?? DEF_RETRY_DELAY;
|
|
27
|
+
let last;
|
|
28
|
+
for (let attempt = 1; attempt <= retries; ++attempt) {
|
|
29
|
+
try {
|
|
30
|
+
await pool.query('SELECT 1');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
last = error;
|
|
35
|
+
if (attempt === retries || !isTransient(error)) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
console.log(`${location}: not ready (${attempt}/${retries}), retrying in ${delay}ms…`);
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const translated = pgErrorToResourceError(last);
|
|
43
|
+
const failure = new PostgresConnectionError(`unreachable:${location}:${translated.message}`);
|
|
44
|
+
failure.cause = last;
|
|
45
|
+
throw failure;
|
|
46
|
+
};
|
|
47
|
+
/** `CREATE SCHEMA IF NOT EXISTS` on a pooled connection. */
|
|
48
|
+
export const ensureSchema = async (pool, schema) => {
|
|
49
|
+
try {
|
|
50
|
+
await pool.query(`CREATE SCHEMA IF NOT EXISTS ${quoteIdent(schema)}`);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw pgErrorToResourceError(error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/utils/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAIzG,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAEnF;;;;GAIG;AACH,MAAM,WAAW,GAAG,CAAC,KAAc,EAAW,EAAE;IAC9C,MAAM,IAAI,GAAI,KAAkC,EAAE,IAAI,CAAA;IACtD,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACjB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,CAAC,sBAAsB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;AAC/C,CAAC,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,EAAE,IAAU,EAAE,IAAkB,EAAE,QAAgB,EAAiB,EAAE;IAC7F,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,IAAI,WAAW,CAAC,CAAA;IACxD,MAAM,KAAK,GAAG,IAAI,CAAC,gBAAgB,IAAI,eAAe,CAAA;IACtD,IAAI,IAAa,CAAA;IAEjB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC;QACpD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;YAE5B,OAAM;QACR,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,GAAG,KAAK,CAAA;YACZ,IAAI,OAAO,KAAK,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC/C,MAAK;YACP,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,GAAG,QAAQ,gBAAgB,OAAO,IAAI,OAAO,kBAAkB,KAAK,KAAK,CAAC,CAAA;YACtF,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAA;QAC1D,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAA;IAC/C,MAAM,OAAO,GAAG,IAAI,uBAAuB,CAAC,eAAe,QAAQ,IAAI,UAAU,CAAC,OAAO,EAAE,CAAC,CAAA;IAC5F,OAAO,CAAC,KAAK,GAAG,IAAI,CAAA;IAEpB,MAAM,OAAO,CAAA;AACf,CAAC,CAAA;AAED,4DAA4D;AAC5D,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,IAAU,EAAE,MAAc,EAAiB,EAAE;IAC9E,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,KAAK,CAAC,+BAA+B,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACvE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,sBAAsB,CAAC,KAAK,CAAC,CAAA;IACrC,CAAC;AACH,CAAC,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/postgres",
|
|
3
|
+
"version": "0.1.15",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc -b",
|
|
8
|
+
"dev": "sleep 210 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
9
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty",
|
|
10
|
+
"test": "bun test ./tests"
|
|
11
|
+
},
|
|
12
|
+
"main": "build/index.js",
|
|
13
|
+
"module": "build/index.js",
|
|
14
|
+
"types": "build/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./build/index.js",
|
|
18
|
+
"require": "./build/index.js",
|
|
19
|
+
"default": "./build/index.js",
|
|
20
|
+
"module": "./build/index.js",
|
|
21
|
+
"types": "./build/index.d.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@owlmeans/basic-keys": "^0.1.15",
|
|
26
|
+
"@owlmeans/context": "^0.1.15",
|
|
27
|
+
"@owlmeans/postgres-resource": "^0.1.15",
|
|
28
|
+
"@owlmeans/resource": "^0.1.15",
|
|
29
|
+
"@owlmeans/server-context": "^0.1.15",
|
|
30
|
+
"drizzle-orm": "~0.45.2",
|
|
31
|
+
"pg": "^8.22.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
35
|
+
"@owlmeans/test-integration": "^0.1.15",
|
|
36
|
+
"@types/bun": "^1.3.14",
|
|
37
|
+
"@types/node": "^26.1.0",
|
|
38
|
+
"@types/pg": "^8.20.4",
|
|
39
|
+
"nodemon": "^3.1.14",
|
|
40
|
+
"typescript": "^6.0.3"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|