@rvoh/dream 2.11.1 → 2.11.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/dist/cjs/src/db/DreamDbConnection.js +45 -1
- package/dist/cjs/src/db/dbConnectionLeakDiagnostics.js +117 -0
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +17 -4
- package/dist/cjs/src/errors/dream-app/MissingDbSslDirective.js +1 -1
- package/dist/esm/src/db/DreamDbConnection.js +45 -1
- package/dist/esm/src/db/dbConnectionLeakDiagnostics.js +117 -0
- package/dist/esm/src/dream/QueryDriver/Kysely.js +17 -4
- package/dist/esm/src/errors/dream-app/MissingDbSslDirective.js +1 -1
- package/dist/types/src/db/DreamDbConnection.d.ts +2 -2
- package/dist/types/src/db/dbConnectionLeakDiagnostics.d.ts +10 -0
- package/dist/types/src/db/index.d.ts +2 -2
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +2 -2
- package/dist/types/src/dream-app/index.d.ts +25 -4
- package/dist/types/src/package-exports/types.d.ts +1 -1
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +9 -9
- package/docs/classes/db.KyselyQueryDriver.html +32 -32
- package/docs/classes/db.PostgresQueryDriver.html +33 -33
- package/docs/classes/db.QueryDriverBase.html +31 -31
- package/docs/classes/errors.CheckConstraintViolation.html +3 -3
- package/docs/classes/errors.ColumnOverflow.html +3 -3
- package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
- package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
- package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
- package/docs/classes/errors.DecryptionError.html +2 -2
- package/docs/classes/errors.DecryptionParseError.html +2 -2
- package/docs/classes/errors.DecryptionRotationError.html +3 -3
- package/docs/classes/errors.GlobalNameNotSet.html +3 -3
- package/docs/classes/errors.InvalidCalendarDate.html +2 -2
- package/docs/classes/errors.InvalidClockTime.html +2 -2
- package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
- package/docs/classes/errors.InvalidDateTime.html +2 -2
- package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
- package/docs/classes/errors.NonLoadedAssociation.html +3 -3
- package/docs/classes/errors.NotNullViolation.html +3 -3
- package/docs/classes/errors.RecordNotFound.html +3 -3
- package/docs/classes/errors.ValidationError.html +3 -3
- package/docs/classes/index.CalendarDate.html +33 -33
- package/docs/classes/index.ClockTime.html +32 -32
- package/docs/classes/index.ClockTimeTz.html +35 -35
- package/docs/classes/index.DateTime.html +86 -86
- package/docs/classes/index.Decorators.html +19 -19
- package/docs/classes/index.Dream.html +118 -118
- package/docs/classes/index.DreamApp.html +5 -5
- package/docs/classes/index.DreamTransaction.html +2 -2
- package/docs/classes/index.Env.html +2 -2
- package/docs/classes/index.Query.html +56 -56
- package/docs/classes/system.CliFileWriter.html +4 -4
- package/docs/classes/system.DreamBin.html +2 -2
- package/docs/classes/system.DreamCLI.html +7 -7
- package/docs/classes/system.DreamImporter.html +2 -2
- package/docs/classes/system.DreamLogos.html +2 -2
- package/docs/classes/system.DreamSerializerBuilder.html +11 -11
- package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
- package/docs/classes/system.PathHelpers.html +3 -3
- package/docs/classes/utils.Encrypt.html +3 -3
- package/docs/classes/utils.Range.html +2 -2
- package/docs/functions/db.closeAllDbConnections.html +1 -1
- package/docs/functions/db.dreamDbConnections.html +1 -1
- package/docs/functions/db.untypedDb.html +1 -1
- package/docs/functions/db.validateColumn.html +1 -1
- package/docs/functions/db.validateTable.html +1 -1
- package/docs/functions/errors.pgErrorType.html +1 -1
- package/docs/functions/index.DreamSerializer.html +1 -1
- package/docs/functions/index.ObjectSerializer.html +1 -1
- package/docs/functions/index.ReplicaSafe.html +1 -1
- package/docs/functions/index.STI.html +1 -1
- package/docs/functions/index.SoftDelete.html +1 -1
- package/docs/functions/utils.camelize.html +1 -1
- package/docs/functions/utils.capitalize.html +1 -1
- package/docs/functions/utils.cloneDeepSafe.html +1 -1
- package/docs/functions/utils.compact.html +1 -1
- package/docs/functions/utils.groupBy.html +1 -1
- package/docs/functions/utils.hyphenize.html +1 -1
- package/docs/functions/utils.intersection.html +1 -1
- package/docs/functions/utils.isEmpty.html +1 -1
- package/docs/functions/utils.normalizeUnicode.html +1 -1
- package/docs/functions/utils.pascalize.html +1 -1
- package/docs/functions/utils.percent.html +1 -1
- package/docs/functions/utils.range.html +1 -1
- package/docs/functions/utils.round.html +1 -1
- package/docs/functions/utils.sanitizeString.html +1 -1
- package/docs/functions/utils.snakeify.html +1 -1
- package/docs/functions/utils.sort.html +1 -1
- package/docs/functions/utils.sortBy.html +1 -1
- package/docs/functions/utils.sortObjectByKey.html +1 -1
- package/docs/functions/utils.sortObjectByValue.html +1 -1
- package/docs/functions/utils.uncapitalize.html +1 -1
- package/docs/functions/utils.uniq.html +1 -1
- package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
- package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
- package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
- package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
- package/docs/interfaces/types.BelongsToStatement.html +2 -2
- package/docs/interfaces/types.DecoratorContext.html +2 -2
- package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
- package/docs/interfaces/types.DreamAppOpts.html +2 -2
- package/docs/interfaces/types.DreamDbConfig.html +47 -0
- package/docs/interfaces/types.DurationObject.html +2 -2
- package/docs/interfaces/types.EncryptOptions.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
- package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
- package/docs/modules/types.html +1 -1
- package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
- package/docs/types/openapi.OpenapiAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiFormats.html +1 -1
- package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
- package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
- package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaString.html +1 -1
- package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiTypeField.html +1 -1
- package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
- package/docs/types/types.CalendarDateDurationUnit.html +1 -1
- package/docs/types/types.CalendarDateObject.html +1 -1
- package/docs/types/types.Camelized.html +1 -1
- package/docs/types/types.ClockTimeObject.html +1 -1
- package/docs/types/types.DbConnectionType.html +1 -1
- package/docs/types/types.DbTypes.html +1 -1
- package/docs/types/types.DreamAssociationMetadata.html +1 -1
- package/docs/types/types.DreamAttributes.html +1 -1
- package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
- package/docs/types/types.DreamClassColumn.html +1 -1
- package/docs/types/types.DreamColumn.html +1 -1
- package/docs/types/types.DreamColumnNames.html +1 -1
- package/docs/types/types.DreamLogLevel.html +1 -1
- package/docs/types/types.DreamLogger.html +2 -2
- package/docs/types/types.DreamModelSerializerType.html +1 -1
- package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
- package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
- package/docs/types/types.DreamParamSafeAttributes.html +1 -1
- package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
- package/docs/types/types.DreamSerializable.html +1 -1
- package/docs/types/types.DreamSerializableArray.html +1 -1
- package/docs/types/types.DreamSerializerKey.html +1 -1
- package/docs/types/types.DreamSerializers.html +1 -1
- package/docs/types/types.DreamVirtualColumns.html +1 -1
- package/docs/types/types.DurationUnit.html +1 -1
- package/docs/types/types.EncryptAlgorithm.html +1 -1
- package/docs/types/types.HasManyStatement.html +1 -1
- package/docs/types/types.HasOneStatement.html +1 -1
- package/docs/types/types.Hyphenized.html +1 -1
- package/docs/types/types.Pascalized.html +1 -1
- package/docs/types/types.PrimaryKeyType.html +1 -1
- package/docs/types/types.RoundingPrecision.html +1 -1
- package/docs/types/types.SerializerCasing.html +1 -1
- package/docs/types/types.SimpleObjectSerializerType.html +1 -1
- package/docs/types/types.Snakeified.html +1 -1
- package/docs/types/types.StrictInterface.html +1 -1
- package/docs/types/types.UpdateableAssociationProperties.html +1 -1
- package/docs/types/types.UpdateableProperties.html +1 -1
- package/docs/types/types.ValidationType.html +1 -1
- package/docs/types/types.ViewModel.html +2 -2
- package/docs/types/types.ViewModelClass.html +1 -1
- package/docs/types/types.WeekdayName.html +1 -1
- package/docs/types/types.WhereStatementForDream.html +1 -1
- package/docs/types/types.WhereStatementForDreamClass.html +1 -1
- package/docs/variables/index.DreamConst.html +1 -1
- package/docs/variables/index.ops.html +1 -1
- package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
- package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
- package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
- package/docs/variables/system.primaryKeyTypes.html +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { CamelCasePlugin, Kysely } from 'kysely';
|
|
10
10
|
import DreamApp from '../dream-app/index.js';
|
|
11
11
|
import protectAgainstPollutingAssignment from '../helpers/protectAgainstPollutingAssignment.js';
|
|
12
|
+
import { installDbConnectionLeakDiagnosticsIfEnabled, reportLeakedDbConnections, } from './dbConnectionLeakDiagnostics.js';
|
|
12
13
|
let connections = {};
|
|
13
14
|
export default class DreamDbConnection {
|
|
14
15
|
static getConnection(connectionName, connectionType, dialectProvider) {
|
|
@@ -19,6 +20,9 @@ export default class DreamDbConnection {
|
|
|
19
20
|
return connection;
|
|
20
21
|
}
|
|
21
22
|
const connectionConf = dreamApp.dbConnectionConfig(connectionName, connectionType);
|
|
23
|
+
// Must run before dialectProvider() constructs the pg.Pool. Idempotent and
|
|
24
|
+
// a no-op unless DREAM_DB_LEAK_DIAGNOSTICS is set.
|
|
25
|
+
installDbConnectionLeakDiagnosticsIfEnabled();
|
|
22
26
|
const dbConn = new Kysely({
|
|
23
27
|
log(event) {
|
|
24
28
|
const dreamApp = DreamApp.getOrFail();
|
|
@@ -50,11 +54,51 @@ export async function closeAllDbConnections() {
|
|
|
50
54
|
}
|
|
51
55
|
connections = {};
|
|
52
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Upper bound (ms) on how long {@link closeAllConnectionsForConnectionName}
|
|
59
|
+
* waits for a single Kysely connection's underlying pool to drain.
|
|
60
|
+
*
|
|
61
|
+
* `conn.destroy()` resolves to `pg`'s `pool.end()`, which only settles once
|
|
62
|
+
* every checked-out client has been released back to the pool. A client that
|
|
63
|
+
* was leased by a query still in flight when shutdown began — an aborted HTTP
|
|
64
|
+
* request during a SIGTERM drain, a feature-spec whose page is torn down
|
|
65
|
+
* mid-request — is never released, so `pool.end()` blocks forever and takes
|
|
66
|
+
* the whole shutdown with it. Bounding the wait keeps shutdown deterministic;
|
|
67
|
+
* the leaked socket is reaped by the OS when the process exits.
|
|
68
|
+
*/
|
|
69
|
+
const DB_CONNECTION_CLOSE_TIMEOUT_MS = 10_000;
|
|
53
70
|
export async function closeAllConnectionsForConnectionName(connectionName) {
|
|
54
71
|
const protectedName = protectAgainstPollutingAssignment(connectionName);
|
|
55
72
|
return await Promise.allSettled(Object.keys(connections[protectedName]).map(async (key) => {
|
|
56
73
|
const conn = connections[protectedName][key];
|
|
57
|
-
|
|
74
|
+
// Remove from the registry first so a subsequent getConnection() builds a
|
|
75
|
+
// fresh pool even if this drain times out and the old pool is abandoned.
|
|
58
76
|
delete connections[protectedName][key];
|
|
77
|
+
await destroyConnectionWithinTimeout(conn, `${connectionName}:${key}`);
|
|
59
78
|
}));
|
|
60
79
|
}
|
|
80
|
+
async function destroyConnectionWithinTimeout(conn, label) {
|
|
81
|
+
let timer;
|
|
82
|
+
const timedOut = Symbol('timedOut');
|
|
83
|
+
const timeout = new Promise(resolve => {
|
|
84
|
+
timer = setTimeout(() => resolve(timedOut), DB_CONNECTION_CLOSE_TIMEOUT_MS);
|
|
85
|
+
// Don't let the timer itself keep the event loop (or `process.exit`) alive.
|
|
86
|
+
timer.unref?.();
|
|
87
|
+
});
|
|
88
|
+
try {
|
|
89
|
+
const result = await Promise.race([conn.destroy(), timeout]);
|
|
90
|
+
if (result === timedOut) {
|
|
91
|
+
DreamApp.logWithLevel('warn', `[dream] timed out after ${DB_CONNECTION_CLOSE_TIMEOUT_MS}ms waiting for db connection "${label}" ` +
|
|
92
|
+
`to close; abandoning the drain so shutdown can proceed. A pooled client was most likely held ` +
|
|
93
|
+
`past shutdown by an in-flight or aborted query. ` +
|
|
94
|
+
`Re-run with NODE_DEBUG=dream to log the acquire stack of the offending query.`);
|
|
95
|
+
// No-op unless NODE_DEBUG=dream; when set, this prints the acquire
|
|
96
|
+
// stack(s) of the client(s) that never got released.
|
|
97
|
+
reportLeakedDbConnections(label);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (timer)
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// after building for esm, importing pg using the following:
|
|
2
|
+
//
|
|
3
|
+
// import * as pg from 'pg'
|
|
4
|
+
//
|
|
5
|
+
// will crash. This is difficult to discover, since it only happens
|
|
6
|
+
// when being imported from our esm build.
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
import pg from 'pg';
|
|
10
|
+
import { debuglog } from 'node:util';
|
|
11
|
+
import DreamApp from '../dream-app/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* Opt-in connection-leak diagnostics.
|
|
14
|
+
*
|
|
15
|
+
* `closeAllConnectionsForConnectionName` bounds the shutdown drain, but when it
|
|
16
|
+
* times out it can only say *that* a pooled client was held past shutdown, not
|
|
17
|
+
* *where* it was acquired. Finding the offending query then means hand-patching
|
|
18
|
+
* `pg` — exactly the multi-hour spelunk this is meant to delete.
|
|
19
|
+
*
|
|
20
|
+
* Enable with `NODE_DEBUG=dream` (the same `node:util` debug-channel
|
|
21
|
+
* convention Psychic uses for `NODE_DEBUG=psychic`). Dev/CI only — it patches
|
|
22
|
+
* `pg.Pool.prototype.connect` process-wide and retains references to
|
|
23
|
+
* checked-out clients, so it is never installed unless explicitly asked for.
|
|
24
|
+
* When the close timeout fires, the acquire stack of every still-checked-out
|
|
25
|
+
* client is logged.
|
|
26
|
+
*
|
|
27
|
+
* Like the rest of the ecosystem's `debuglog` usage, the channel is resolved
|
|
28
|
+
* once by Node on first call and cached for the process lifetime — this is a
|
|
29
|
+
* process-level switch, not a runtime toggle.
|
|
30
|
+
*/
|
|
31
|
+
// Resolved once at module load (Node caches the channel), mirroring
|
|
32
|
+
// `const debugEnabled = debuglog('psychic').enabled` in Psychic.
|
|
33
|
+
const dreamDebugEnabled = debuglog('dream').enabled;
|
|
34
|
+
// Map (not WeakMap) so entries can be enumerated at report time. Entries are
|
|
35
|
+
// deleted on release, so only genuinely-leaked clients accumulate, and only
|
|
36
|
+
// when diagnostics are explicitly enabled.
|
|
37
|
+
const checkedOutClients = new Map();
|
|
38
|
+
const INSTALLED = Symbol.for('dream:dbLeakDiagnosticsInstalled');
|
|
39
|
+
let enabled = false;
|
|
40
|
+
function track(client, stack) {
|
|
41
|
+
if (!client)
|
|
42
|
+
return;
|
|
43
|
+
checkedOutClients.set(client, { stack: stack ?? '(no stack captured)', since: Date.now() });
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
const c = client;
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
47
|
+
if (c.__dreamReleasePatched)
|
|
48
|
+
return;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
50
|
+
const originalRelease = c.release;
|
|
51
|
+
if (typeof originalRelease !== 'function')
|
|
52
|
+
return;
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
54
|
+
c.__dreamReleasePatched = true;
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
56
|
+
c.release = function patchedRelease(...args) {
|
|
57
|
+
checkedOutClients.delete(client);
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
|
|
59
|
+
return originalRelease.apply(this, args);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Idempotent. Patches `pg.Pool.prototype.connect` to stamp the caller's stack
|
|
64
|
+
* onto each leased client. No-op unless `DREAM_DB_LEAK_DIAGNOSTICS` is set.
|
|
65
|
+
*/
|
|
66
|
+
export function installDbConnectionLeakDiagnosticsIfEnabled() {
|
|
67
|
+
if (!dreamDebugEnabled)
|
|
68
|
+
return;
|
|
69
|
+
enabled = true;
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
|
|
71
|
+
const Pool = pg.Pool;
|
|
72
|
+
if (!Pool?.prototype)
|
|
73
|
+
return;
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
75
|
+
if (Pool.prototype[INSTALLED])
|
|
76
|
+
return;
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
78
|
+
Pool.prototype[INSTALLED] = true;
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
|
|
80
|
+
const originalConnect = Pool.prototype.connect;
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
82
|
+
Pool.prototype.connect = function patchedConnect(cb) {
|
|
83
|
+
const acquireStack = new Error('db connection acquired here').stack;
|
|
84
|
+
if (typeof cb === 'function') {
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
|
|
86
|
+
return originalConnect.call(this, (err, client, release) => {
|
|
87
|
+
track(client, acquireStack);
|
|
88
|
+
// `track()` replaces `client.release` with an untracking wrapper.
|
|
89
|
+
// Callback-style users call the 3rd `release` arg, which pg captured
|
|
90
|
+
// *before* that swap — so hand them the wrapped one instead, or a
|
|
91
|
+
// correctly-released client would be reported as a false leak.
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
93
|
+
const wrappedRelease = client ? client.release : release;
|
|
94
|
+
cb(err, client, wrappedRelease);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
98
|
+
return originalConnect.call(this).then((client) => {
|
|
99
|
+
track(client, acquireStack);
|
|
100
|
+
return client;
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* If diagnostics are enabled, log the acquire stack of every client still
|
|
106
|
+
* checked out. Called from the shutdown-drain timeout path.
|
|
107
|
+
*/
|
|
108
|
+
export function reportLeakedDbConnections(label) {
|
|
109
|
+
if (!enabled || checkedOutClients.size === 0)
|
|
110
|
+
return;
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const report = Array.from(checkedOutClients.values())
|
|
113
|
+
.map((entry, i) => ` [#${i}] held ${now - entry.since}ms\n${entry.stack}`)
|
|
114
|
+
.join('\n\n');
|
|
115
|
+
DreamApp.logWithLevel('warn', `[dream] connection-leak diagnostics: ${checkedOutClients.size} pg client(s) still checked out ` +
|
|
116
|
+
`when "${label}" hit the close timeout. Acquired at:\n\n${report}`);
|
|
117
|
+
}
|
|
@@ -92,16 +92,29 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
92
92
|
return _db(connectionName || 'default', dbConnectionType, this.dialectProvider(connectionName, dbConnectionType));
|
|
93
93
|
}
|
|
94
94
|
static dialectProvider(connectionName, dbConnectionType) {
|
|
95
|
-
return (connectionConf) =>
|
|
96
|
-
pool
|
|
95
|
+
return (connectionConf) => {
|
|
96
|
+
const pool = new pg.Pool({
|
|
97
|
+
// Spread pg passthrough first; Dream's resolved fields follow and
|
|
98
|
+
// always win (per-connection database name, TLS directive).
|
|
99
|
+
...(connectionConf.pg ?? {}),
|
|
97
100
|
user: connectionConf.user || '',
|
|
98
101
|
password: connectionConf.password || '',
|
|
99
102
|
database: DreamApp.getOrFail().dbName(connectionName, dbConnectionType),
|
|
100
103
|
host: connectionConf.host || 'localhost',
|
|
101
104
|
port: connectionConf.port || 5432,
|
|
102
105
|
ssl: resolvePostgresSsl(connectionConf),
|
|
103
|
-
})
|
|
104
|
-
|
|
106
|
+
});
|
|
107
|
+
// node-postgres explicitly warns: a Pool that emits 'error' with no
|
|
108
|
+
// listener crashes the Node process. Idle pooled clients emit 'error'
|
|
109
|
+
// on backend restart / failover / a load balancer reaping idle TCP.
|
|
110
|
+
// Log via Dream's logger instead of taking the process down; the pool
|
|
111
|
+
// discards the dead client and recovers on the next acquire.
|
|
112
|
+
pool.on('error', err => {
|
|
113
|
+
DreamApp.getOrFail().logger.error(`[dream] idle pg client error on connection "${connectionName}:${dbConnectionType}" ` +
|
|
114
|
+
`(handled — process kept alive): ${err.stack ?? String(err)}`);
|
|
115
|
+
});
|
|
116
|
+
return new PostgresDialect({ pool });
|
|
117
|
+
};
|
|
105
118
|
}
|
|
106
119
|
static async ensureAllMigrationsHaveBeenRun(connectionName) {
|
|
107
120
|
const migrationsNeedToBeRun = await checkForNeedToBeRunMigrations({
|
|
@@ -9,7 +9,7 @@ export default class MissingDbSslDirective extends Error {
|
|
|
9
9
|
get message() {
|
|
10
10
|
return `
|
|
11
11
|
DreamApp refused to register a db credential without an explicit TLS
|
|
12
|
-
directive. Every \`
|
|
12
|
+
directive. Every \`DreamDbConfig\` passed to \`app.set('db', ...)\`
|
|
13
13
|
must set one of:
|
|
14
14
|
|
|
15
15
|
ssl: { rejectUnauthorized: true } // verified TLS (system CA)
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { CamelCasePlugin, Kysely } from 'kysely';
|
|
10
10
|
import DreamApp from '../dream-app/index.js';
|
|
11
11
|
import protectAgainstPollutingAssignment from '../helpers/protectAgainstPollutingAssignment.js';
|
|
12
|
+
import { installDbConnectionLeakDiagnosticsIfEnabled, reportLeakedDbConnections, } from './dbConnectionLeakDiagnostics.js';
|
|
12
13
|
let connections = {};
|
|
13
14
|
export default class DreamDbConnection {
|
|
14
15
|
static getConnection(connectionName, connectionType, dialectProvider) {
|
|
@@ -19,6 +20,9 @@ export default class DreamDbConnection {
|
|
|
19
20
|
return connection;
|
|
20
21
|
}
|
|
21
22
|
const connectionConf = dreamApp.dbConnectionConfig(connectionName, connectionType);
|
|
23
|
+
// Must run before dialectProvider() constructs the pg.Pool. Idempotent and
|
|
24
|
+
// a no-op unless DREAM_DB_LEAK_DIAGNOSTICS is set.
|
|
25
|
+
installDbConnectionLeakDiagnosticsIfEnabled();
|
|
22
26
|
const dbConn = new Kysely({
|
|
23
27
|
log(event) {
|
|
24
28
|
const dreamApp = DreamApp.getOrFail();
|
|
@@ -50,11 +54,51 @@ export async function closeAllDbConnections() {
|
|
|
50
54
|
}
|
|
51
55
|
connections = {};
|
|
52
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Upper bound (ms) on how long {@link closeAllConnectionsForConnectionName}
|
|
59
|
+
* waits for a single Kysely connection's underlying pool to drain.
|
|
60
|
+
*
|
|
61
|
+
* `conn.destroy()` resolves to `pg`'s `pool.end()`, which only settles once
|
|
62
|
+
* every checked-out client has been released back to the pool. A client that
|
|
63
|
+
* was leased by a query still in flight when shutdown began — an aborted HTTP
|
|
64
|
+
* request during a SIGTERM drain, a feature-spec whose page is torn down
|
|
65
|
+
* mid-request — is never released, so `pool.end()` blocks forever and takes
|
|
66
|
+
* the whole shutdown with it. Bounding the wait keeps shutdown deterministic;
|
|
67
|
+
* the leaked socket is reaped by the OS when the process exits.
|
|
68
|
+
*/
|
|
69
|
+
const DB_CONNECTION_CLOSE_TIMEOUT_MS = 10_000;
|
|
53
70
|
export async function closeAllConnectionsForConnectionName(connectionName) {
|
|
54
71
|
const protectedName = protectAgainstPollutingAssignment(connectionName);
|
|
55
72
|
return await Promise.allSettled(Object.keys(connections[protectedName]).map(async (key) => {
|
|
56
73
|
const conn = connections[protectedName][key];
|
|
57
|
-
|
|
74
|
+
// Remove from the registry first so a subsequent getConnection() builds a
|
|
75
|
+
// fresh pool even if this drain times out and the old pool is abandoned.
|
|
58
76
|
delete connections[protectedName][key];
|
|
77
|
+
await destroyConnectionWithinTimeout(conn, `${connectionName}:${key}`);
|
|
59
78
|
}));
|
|
60
79
|
}
|
|
80
|
+
async function destroyConnectionWithinTimeout(conn, label) {
|
|
81
|
+
let timer;
|
|
82
|
+
const timedOut = Symbol('timedOut');
|
|
83
|
+
const timeout = new Promise(resolve => {
|
|
84
|
+
timer = setTimeout(() => resolve(timedOut), DB_CONNECTION_CLOSE_TIMEOUT_MS);
|
|
85
|
+
// Don't let the timer itself keep the event loop (or `process.exit`) alive.
|
|
86
|
+
timer.unref?.();
|
|
87
|
+
});
|
|
88
|
+
try {
|
|
89
|
+
const result = await Promise.race([conn.destroy(), timeout]);
|
|
90
|
+
if (result === timedOut) {
|
|
91
|
+
DreamApp.logWithLevel('warn', `[dream] timed out after ${DB_CONNECTION_CLOSE_TIMEOUT_MS}ms waiting for db connection "${label}" ` +
|
|
92
|
+
`to close; abandoning the drain so shutdown can proceed. A pooled client was most likely held ` +
|
|
93
|
+
`past shutdown by an in-flight or aborted query. ` +
|
|
94
|
+
`Re-run with NODE_DEBUG=dream to log the acquire stack of the offending query.`);
|
|
95
|
+
// No-op unless NODE_DEBUG=dream; when set, this prints the acquire
|
|
96
|
+
// stack(s) of the client(s) that never got released.
|
|
97
|
+
reportLeakedDbConnections(label);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (timer)
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// after building for esm, importing pg using the following:
|
|
2
|
+
//
|
|
3
|
+
// import * as pg from 'pg'
|
|
4
|
+
//
|
|
5
|
+
// will crash. This is difficult to discover, since it only happens
|
|
6
|
+
// when being imported from our esm build.
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
import pg from 'pg';
|
|
10
|
+
import { debuglog } from 'node:util';
|
|
11
|
+
import DreamApp from '../dream-app/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* Opt-in connection-leak diagnostics.
|
|
14
|
+
*
|
|
15
|
+
* `closeAllConnectionsForConnectionName` bounds the shutdown drain, but when it
|
|
16
|
+
* times out it can only say *that* a pooled client was held past shutdown, not
|
|
17
|
+
* *where* it was acquired. Finding the offending query then means hand-patching
|
|
18
|
+
* `pg` — exactly the multi-hour spelunk this is meant to delete.
|
|
19
|
+
*
|
|
20
|
+
* Enable with `NODE_DEBUG=dream` (the same `node:util` debug-channel
|
|
21
|
+
* convention Psychic uses for `NODE_DEBUG=psychic`). Dev/CI only — it patches
|
|
22
|
+
* `pg.Pool.prototype.connect` process-wide and retains references to
|
|
23
|
+
* checked-out clients, so it is never installed unless explicitly asked for.
|
|
24
|
+
* When the close timeout fires, the acquire stack of every still-checked-out
|
|
25
|
+
* client is logged.
|
|
26
|
+
*
|
|
27
|
+
* Like the rest of the ecosystem's `debuglog` usage, the channel is resolved
|
|
28
|
+
* once by Node on first call and cached for the process lifetime — this is a
|
|
29
|
+
* process-level switch, not a runtime toggle.
|
|
30
|
+
*/
|
|
31
|
+
// Resolved once at module load (Node caches the channel), mirroring
|
|
32
|
+
// `const debugEnabled = debuglog('psychic').enabled` in Psychic.
|
|
33
|
+
const dreamDebugEnabled = debuglog('dream').enabled;
|
|
34
|
+
// Map (not WeakMap) so entries can be enumerated at report time. Entries are
|
|
35
|
+
// deleted on release, so only genuinely-leaked clients accumulate, and only
|
|
36
|
+
// when diagnostics are explicitly enabled.
|
|
37
|
+
const checkedOutClients = new Map();
|
|
38
|
+
const INSTALLED = Symbol.for('dream:dbLeakDiagnosticsInstalled');
|
|
39
|
+
let enabled = false;
|
|
40
|
+
function track(client, stack) {
|
|
41
|
+
if (!client)
|
|
42
|
+
return;
|
|
43
|
+
checkedOutClients.set(client, { stack: stack ?? '(no stack captured)', since: Date.now() });
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
const c = client;
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
47
|
+
if (c.__dreamReleasePatched)
|
|
48
|
+
return;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
50
|
+
const originalRelease = c.release;
|
|
51
|
+
if (typeof originalRelease !== 'function')
|
|
52
|
+
return;
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
54
|
+
c.__dreamReleasePatched = true;
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
56
|
+
c.release = function patchedRelease(...args) {
|
|
57
|
+
checkedOutClients.delete(client);
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
|
|
59
|
+
return originalRelease.apply(this, args);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Idempotent. Patches `pg.Pool.prototype.connect` to stamp the caller's stack
|
|
64
|
+
* onto each leased client. No-op unless `DREAM_DB_LEAK_DIAGNOSTICS` is set.
|
|
65
|
+
*/
|
|
66
|
+
export function installDbConnectionLeakDiagnosticsIfEnabled() {
|
|
67
|
+
if (!dreamDebugEnabled)
|
|
68
|
+
return;
|
|
69
|
+
enabled = true;
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
|
|
71
|
+
const Pool = pg.Pool;
|
|
72
|
+
if (!Pool?.prototype)
|
|
73
|
+
return;
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
75
|
+
if (Pool.prototype[INSTALLED])
|
|
76
|
+
return;
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
78
|
+
Pool.prototype[INSTALLED] = true;
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
|
|
80
|
+
const originalConnect = Pool.prototype.connect;
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
82
|
+
Pool.prototype.connect = function patchedConnect(cb) {
|
|
83
|
+
const acquireStack = new Error('db connection acquired here').stack;
|
|
84
|
+
if (typeof cb === 'function') {
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
|
|
86
|
+
return originalConnect.call(this, (err, client, release) => {
|
|
87
|
+
track(client, acquireStack);
|
|
88
|
+
// `track()` replaces `client.release` with an untracking wrapper.
|
|
89
|
+
// Callback-style users call the 3rd `release` arg, which pg captured
|
|
90
|
+
// *before* that swap — so hand them the wrapped one instead, or a
|
|
91
|
+
// correctly-released client would be reported as a false leak.
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
93
|
+
const wrappedRelease = client ? client.release : release;
|
|
94
|
+
cb(err, client, wrappedRelease);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
98
|
+
return originalConnect.call(this).then((client) => {
|
|
99
|
+
track(client, acquireStack);
|
|
100
|
+
return client;
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* If diagnostics are enabled, log the acquire stack of every client still
|
|
106
|
+
* checked out. Called from the shutdown-drain timeout path.
|
|
107
|
+
*/
|
|
108
|
+
export function reportLeakedDbConnections(label) {
|
|
109
|
+
if (!enabled || checkedOutClients.size === 0)
|
|
110
|
+
return;
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const report = Array.from(checkedOutClients.values())
|
|
113
|
+
.map((entry, i) => ` [#${i}] held ${now - entry.since}ms\n${entry.stack}`)
|
|
114
|
+
.join('\n\n');
|
|
115
|
+
DreamApp.logWithLevel('warn', `[dream] connection-leak diagnostics: ${checkedOutClients.size} pg client(s) still checked out ` +
|
|
116
|
+
`when "${label}" hit the close timeout. Acquired at:\n\n${report}`);
|
|
117
|
+
}
|
|
@@ -92,16 +92,29 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
92
92
|
return _db(connectionName || 'default', dbConnectionType, this.dialectProvider(connectionName, dbConnectionType));
|
|
93
93
|
}
|
|
94
94
|
static dialectProvider(connectionName, dbConnectionType) {
|
|
95
|
-
return (connectionConf) =>
|
|
96
|
-
pool
|
|
95
|
+
return (connectionConf) => {
|
|
96
|
+
const pool = new pg.Pool({
|
|
97
|
+
// Spread pg passthrough first; Dream's resolved fields follow and
|
|
98
|
+
// always win (per-connection database name, TLS directive).
|
|
99
|
+
...(connectionConf.pg ?? {}),
|
|
97
100
|
user: connectionConf.user || '',
|
|
98
101
|
password: connectionConf.password || '',
|
|
99
102
|
database: DreamApp.getOrFail().dbName(connectionName, dbConnectionType),
|
|
100
103
|
host: connectionConf.host || 'localhost',
|
|
101
104
|
port: connectionConf.port || 5432,
|
|
102
105
|
ssl: resolvePostgresSsl(connectionConf),
|
|
103
|
-
})
|
|
104
|
-
|
|
106
|
+
});
|
|
107
|
+
// node-postgres explicitly warns: a Pool that emits 'error' with no
|
|
108
|
+
// listener crashes the Node process. Idle pooled clients emit 'error'
|
|
109
|
+
// on backend restart / failover / a load balancer reaping idle TCP.
|
|
110
|
+
// Log via Dream's logger instead of taking the process down; the pool
|
|
111
|
+
// discards the dead client and recovers on the next acquire.
|
|
112
|
+
pool.on('error', err => {
|
|
113
|
+
DreamApp.getOrFail().logger.error(`[dream] idle pg client error on connection "${connectionName}:${dbConnectionType}" ` +
|
|
114
|
+
`(handled — process kept alive): ${err.stack ?? String(err)}`);
|
|
115
|
+
});
|
|
116
|
+
return new PostgresDialect({ pool });
|
|
117
|
+
};
|
|
105
118
|
}
|
|
106
119
|
static async ensureAllMigrationsHaveBeenRun(connectionName) {
|
|
107
120
|
const migrationsNeedToBeRun = await checkForNeedToBeRunMigrations({
|
|
@@ -9,7 +9,7 @@ export default class MissingDbSslDirective extends Error {
|
|
|
9
9
|
get message() {
|
|
10
10
|
return `
|
|
11
11
|
DreamApp refused to register a db credential without an explicit TLS
|
|
12
|
-
directive. Every \`
|
|
12
|
+
directive. Every \`DreamDbConfig\` passed to \`app.set('db', ...)\`
|
|
13
13
|
must set one of:
|
|
14
14
|
|
|
15
15
|
ssl: { rejectUnauthorized: true } // verified TLS (system CA)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Kysely } from 'kysely';
|
|
2
|
-
import {
|
|
2
|
+
import { DreamDbConfig } from '../dream-app/index.js';
|
|
3
3
|
import { DbConnectionType } from '../types/db.js';
|
|
4
4
|
export default class DreamDbConnection {
|
|
5
5
|
static getConnection<DB>(connectionName: string, connectionType: DbConnectionType, dialectProvider: DialectProviderCb): Kysely<DB>;
|
|
@@ -12,4 +12,4 @@ export declare function dreamDbConnections(): {
|
|
|
12
12
|
};
|
|
13
13
|
export declare function closeAllDbConnections(): Promise<void>;
|
|
14
14
|
export declare function closeAllConnectionsForConnectionName(connectionName: string): Promise<PromiseSettledResult<void>[]>;
|
|
15
|
-
export type DialectProviderCb = (connectionConf:
|
|
15
|
+
export type DialectProviderCb = (connectionConf: DreamDbConfig) => any;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent. Patches `pg.Pool.prototype.connect` to stamp the caller's stack
|
|
3
|
+
* onto each leased client. No-op unless `DREAM_DB_LEAK_DIAGNOSTICS` is set.
|
|
4
|
+
*/
|
|
5
|
+
export declare function installDbConnectionLeakDiagnosticsIfEnabled(): void;
|
|
6
|
+
/**
|
|
7
|
+
* If diagnostics are enabled, log the acquire stack of every client still
|
|
8
|
+
* checked out. Called from the shutdown-drain timeout path.
|
|
9
|
+
*/
|
|
10
|
+
export declare function reportLeakedDbConnections(label: string): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Kysely } from 'kysely';
|
|
2
|
-
import {
|
|
2
|
+
import { DreamDbConfig } from '../dream-app/index.js';
|
|
3
3
|
import Dream from '../Dream.js';
|
|
4
4
|
import { DbConnectionType } from '../types/db.js';
|
|
5
|
-
export default function db<T extends Dream, DB extends T['DB'] = T['DB']>(connectionName?: string, connectionType?: DbConnectionType, dialectProvider?: (connectionConf:
|
|
5
|
+
export default function db<T extends Dream, DB extends T['DB'] = T['DB']>(connectionName?: string, connectionType?: DbConnectionType, dialectProvider?: (connectionConf: DreamDbConfig) => any): Kysely<DB>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ConnectionOptions as TlsConnectionOptions } from 'node:tls';
|
|
2
2
|
import { DeleteQueryBuilder, Kysely, Transaction as KyselyTransaction, OrderByItemBuilder, SelectQueryBuilder, UpdateQueryBuilder } from 'kysely';
|
|
3
3
|
import { DialectProviderCb } from '../../db/DreamDbConnection.js';
|
|
4
|
-
import {
|
|
4
|
+
import { DreamDbConfig } from '../../dream-app/index.js';
|
|
5
5
|
import Dream from '../../Dream.js';
|
|
6
6
|
import { SchemaBuilderInformationSchemaRow } from '../../helpers/cli/ASTBuilder.js';
|
|
7
7
|
import { AssociationStatement } from '../../types/associations/shared.js';
|
|
@@ -482,4 +482,4 @@ export default class KyselyQueryDriver<DreamInstance extends Dream> extends Quer
|
|
|
482
482
|
* `app.set('db', ...)` time when both `ssl` and `useSsl` are unset, so this
|
|
483
483
|
* resolver never sees the "neither directive" state.
|
|
484
484
|
*/
|
|
485
|
-
export declare function resolvePostgresSsl(connectionConf:
|
|
485
|
+
export declare function resolvePostgresSsl(connectionConf: DreamDbConfig): TlsConnectionOptions | false;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CompiledQuery } from 'kysely';
|
|
2
2
|
import type { ConnectionOptions as TlsConnectionOptions } from 'node:tls';
|
|
3
|
+
import type { PoolConfig as PgPoolConfig } from 'pg';
|
|
3
4
|
import { Context } from 'node:vm';
|
|
4
5
|
import Dream from '../Dream.js';
|
|
5
6
|
import QueryDriverBase from '../dream/QueryDriver/Base.js';
|
|
@@ -148,7 +149,7 @@ export default class DreamApp {
|
|
|
148
149
|
get models(): Record<string, typeof Dream>;
|
|
149
150
|
get serializers(): Record<string, DreamModelSerializerType | SimpleObjectSerializerType>;
|
|
150
151
|
dbName(connectionName: string, connection: DbConnectionType): string;
|
|
151
|
-
dbConnectionConfig(connectionName: string, connection: DbConnectionType):
|
|
152
|
+
dbConnectionConfig(connectionName: string, connection: DbConnectionType): DreamDbConfig;
|
|
152
153
|
dbConnectionQueryDriverClass<T extends Dream = Dream>(connectionName: string): typeof QueryDriverBase<T>;
|
|
153
154
|
dbConnectionKeys(): string[];
|
|
154
155
|
hasReplicaConfig(connectionName: string): boolean;
|
|
@@ -179,8 +180,8 @@ export interface DreamDirectoryPaths {
|
|
|
179
180
|
types?: string;
|
|
180
181
|
}
|
|
181
182
|
export interface DreamDbCredentialOptions {
|
|
182
|
-
primary:
|
|
183
|
-
replica?:
|
|
183
|
+
primary: DreamDbConfig;
|
|
184
|
+
replica?: DreamDbConfig | undefined;
|
|
184
185
|
queryDriverClass?: typeof QueryDriverBase;
|
|
185
186
|
/**
|
|
186
187
|
* a string which informs the kysely-codegen tool,
|
|
@@ -204,7 +205,7 @@ export interface DreamDbCredentialOptions {
|
|
|
204
205
|
tableExcludePattern?: string;
|
|
205
206
|
}
|
|
206
207
|
type UnicodeNormalizationForm = 'NFC' | 'NFD' | 'none';
|
|
207
|
-
export interface
|
|
208
|
+
export interface DreamDbConfig {
|
|
208
209
|
user: string;
|
|
209
210
|
password: string;
|
|
210
211
|
host: string;
|
|
@@ -242,6 +243,26 @@ export interface SingleDbCredential {
|
|
|
242
243
|
* decision at the call site rather than a silent default.
|
|
243
244
|
*/
|
|
244
245
|
ssl?: TlsConnectionOptions | false;
|
|
246
|
+
/**
|
|
247
|
+
* pg pool/client options passed straight through to `new pg.Pool(...)`.
|
|
248
|
+
* Dream knows nothing about these fields — pg's own types carry the
|
|
249
|
+
* documentation. Unset ⇒ pg applies its own defaults (backward compatible).
|
|
250
|
+
*
|
|
251
|
+
* Omitted from the passthrough:
|
|
252
|
+
* - `user / password / database / host / port / ssl`: Dream manages these
|
|
253
|
+
* (per-connection name, TLS directive) — hard invariants.
|
|
254
|
+
* - `connectionString`: `pg`'s `ConnectionParameters` re-parses the URL
|
|
255
|
+
* and lets its fields take precedence, bypassing Dream's per-connection
|
|
256
|
+
* database name and TLS directive. Parse `DATABASE_URL` into the discrete
|
|
257
|
+
* `host/port/user/password/name/ssl` fields in `conf/dream.ts` instead.
|
|
258
|
+
* - `min`: node-pg's `pg-pool` does not honor it (silent no-op).
|
|
259
|
+
* - `types / Client / Promise / log / stream`: programmatic, not
|
|
260
|
+
* credential config (`types` is already wired via Dream's parsers).
|
|
261
|
+
*
|
|
262
|
+
* When another database adapter is added, a parallel `mysql?: ...` key
|
|
263
|
+
* (or similar) will appear here — this key is intentionally pg-specific.
|
|
264
|
+
*/
|
|
265
|
+
pg?: Omit<PgPoolConfig, 'user' | 'password' | 'database' | 'host' | 'port' | 'ssl' | 'connectionString' | 'min' | 'types' | 'Client' | 'Promise' | 'log' | 'stream'>;
|
|
245
266
|
}
|
|
246
267
|
export type DreamLogger = {
|
|
247
268
|
info: (...args: any[]) => void;
|
|
@@ -14,5 +14,5 @@ export { type DurationObject, type DurationUnit, type WeekdayName } from '../typ
|
|
|
14
14
|
export { type DreamAssociationMetadata, type DreamAttributes, type DreamClassAssociationAndStatement, type DreamClassColumn, type DreamColumn, type DreamColumnNames, type DreamOrViewModelClassSerializerKey, type DreamOrViewModelSerializerKey, type DreamParamSafeAttributes, type DreamParamSafeColumnNames, type DreamSerializable, type DreamSerializableArray, type DreamSerializerKey, type DreamSerializers, type DreamVirtualColumns, type UpdateableAssociationProperties, type UpdateableProperties, type ViewModel, type ViewModelClass, } from '../types/dream.js';
|
|
15
15
|
export { type DreamModelSerializerType, type InternalAnyTypedSerializerRendersMany, type InternalAnyTypedSerializerRendersOne, type SerializerCasing, type SimpleObjectSerializerType, } from '../types/serializer.js';
|
|
16
16
|
export { type ValidationType } from '../types/validation.js';
|
|
17
|
-
export { type DreamAppInitOptions, type DreamAppOpts, type DreamLogger, type DreamLogLevel, } from '../dream-app/index.js';
|
|
17
|
+
export { type DreamAppInitOptions, type DreamAppOpts, type DreamLogger, type DreamLogLevel, type DreamDbConfig, } from '../dream-app/index.js';
|
|
18
18
|
export { type WhereStatementForDream, type WhereStatementForDreamClass, } from '../types/associations/shared.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
window.navigationData = "eJytm01z4zYShv+LzlO7m8kmuzs3WbIT1diW1/Z4Dqk5QGRbwhoEGBDyjCaV/
|
|
1
|
+
window.navigationData = "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"
|