@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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/agent-meta/instructions/postgres.instructions.md +57 -0
  4. package/agent-meta/manifest.json +23 -0
  5. package/agent-meta/skills/postgres/SKILL.md +121 -0
  6. package/build/bootstrap.d.ts +16 -0
  7. package/build/bootstrap.d.ts.map +1 -0
  8. package/build/bootstrap.js +106 -0
  9. package/build/bootstrap.js.map +1 -0
  10. package/build/consts.d.ts +29 -0
  11. package/build/consts.d.ts.map +1 -0
  12. package/build/consts.js +30 -0
  13. package/build/consts.js.map +1 -0
  14. package/build/index.d.ts +8 -0
  15. package/build/index.d.ts.map +1 -0
  16. package/build/index.js +7 -0
  17. package/build/index.js.map +1 -0
  18. package/build/middleware.d.ts +11 -0
  19. package/build/middleware.d.ts.map +1 -0
  20. package/build/middleware.js +21 -0
  21. package/build/middleware.js.map +1 -0
  22. package/build/service.d.ts +9 -0
  23. package/build/service.d.ts.map +1 -0
  24. package/build/service.js +221 -0
  25. package/build/service.js.map +1 -0
  26. package/build/types.d.ts +40 -0
  27. package/build/types.d.ts.map +1 -0
  28. package/build/types.js +2 -0
  29. package/build/types.js.map +1 -0
  30. package/build/utils/config.d.ts +27 -0
  31. package/build/utils/config.d.ts.map +1 -0
  32. package/build/utils/config.js +96 -0
  33. package/build/utils/config.js.map +1 -0
  34. package/build/utils/connection.d.ts +15 -0
  35. package/build/utils/connection.d.ts.map +1 -0
  36. package/build/utils/connection.js +56 -0
  37. package/build/utils/connection.js.map +1 -0
  38. package/package.json +45 -0
  39. package/src/bootstrap.ts +126 -0
  40. package/src/consts.ts +36 -0
  41. package/src/index.ts +7 -0
  42. package/src/middleware.ts +26 -0
  43. package/src/service.ts +287 -0
  44. package/src/types.ts +42 -0
  45. package/src/utils/config.ts +109 -0
  46. package/src/utils/connection.ts +64 -0
  47. package/tests/bootstrap.spec.ts +142 -0
  48. package/tests/context.ts +204 -0
  49. package/tests/crud.spec.ts +227 -0
  50. package/tests/custom-sql.spec.ts +196 -0
  51. package/tests/migration.spec.ts +260 -0
  52. package/tests/sync.spec.ts +156 -0
  53. 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"}
@@ -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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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
+ }