import { CompiledQuery } from 'kysely'; import type { ConnectionOptions as TlsConnectionOptions } from 'node:tls'; import type { PoolConfig as PgPoolConfig } from 'pg'; import { Context } from 'node:vm'; import Dream from '../Dream.js'; import QueryDriverBase from '../dream/QueryDriver/Base.js'; import { EncryptOptions } from '../encrypt/index.js'; import autogeneratedFileDisclaimer from '../helpers/cli/autoGeneratedFileDisclaimer.js'; import modelClassNameFrom from '../helpers/cli/modelClassNameFrom.js'; import globalClassNameFromFullyQualifiedModelName from '../helpers/globalClassNameFromFullyQualifiedModelName.js'; import absoluteDreamPath from '../helpers/path/absoluteDreamPath.js'; import dreamPath from '../helpers/path/dreamPath.js'; import standardizeFullyQualifiedModelName from '../helpers/standardizeFullyQualifiedModelName.js'; import expandStiClasses from '../helpers/sti/expandStiClasses.js'; import inferSerializerFromDreamOrViewModel, { inferSerializersFromDreamClassOrViewModelClass } from '../serializer/helpers/inferSerializerFromDreamOrViewModel.js'; import isDreamSerializer from '../serializer/helpers/isDreamSerializer.js'; import { nullFlattenedOpenapiKeys, nullFlattenedOpenapiKeysOfOtherVariants, nullFlattenedSerializerKeys, nullFlattenedSerializerKeysOfOtherVariants } from '../serializer/helpers/nullFlattenedSerializerKeys.js'; import serializerNameFromFullyQualifiedModelName from '../serializer/helpers/serializerNameFromFullyQualifiedModelName.js'; import { DbConnectionType, LegacyCompatiblePrimaryKeyType } from '../types/db.js'; import { DreamModelSerializerType, SimpleObjectSerializerType } from '../types/serializer.js'; export default class DreamApp { /** * initializes a new dream application and caches it for use * within this processes lifecycle. * * Within dream, we rely on cached information about your app * to be able to serve routes, perform serializer lookups, * generate files, connect to the database, etc... * * In order for this to work properly, the DreamApp#init * function must be called before anything else is called within * Dream. */ static init(cb: (dreamApp: DreamApp) => void | Promise, opts?: Partial & DreamAppInitOptions, deferCb?: (dreamApp: DreamApp) => Promise | void): Promise; /** * Since javascript is inherently vulnerable to circular dependencies, * this function provides a workaround by enabling you to dynamically * bring in classes that, if imported directly, would result in circular * dependency issues. * * NOTE: You should only use this as a last resort, since it can create quite * a headache for you when leaning into your editor to apply renames, etc... * * @param name - the global name you are trying to look up, i.e. 'User', or 'UserSerializer'. * * @example * ```ts * // this pattern is safe from circular imports, since _UserSerializer * // is only being used to type something else, which will not result * // in the circular dependency issue. * * import _UserSerializer from '../serializers/UserSerializer.js' * const UserSerializer = PsychicApp.lookupClassByGlobalName('UserSerializer') as _UserSerializer * ``` */ static lookupClassByGlobalName(name: string): any; /** * @internal * */ static get system(): { globalClassNameFromFullyQualifiedModelName: typeof globalClassNameFromFullyQualifiedModelName; modelClassNameFrom: typeof modelClassNameFrom; absoluteDreamPath: typeof absoluteDreamPath; dreamPath: typeof dreamPath; standardizeFullyQualifiedModelName: typeof standardizeFullyQualifiedModelName; expandStiClasses: typeof expandStiClasses; inferSerializerFromDreamOrViewModel: typeof inferSerializerFromDreamOrViewModel; inferSerializersFromDreamClassOrViewModelClass: typeof inferSerializersFromDreamClassOrViewModelClass; isDreamSerializer: typeof isDreamSerializer; nullFlattenedSerializerKeys: typeof nullFlattenedSerializerKeys; nullFlattenedSerializerKeysOfOtherVariants: typeof nullFlattenedSerializerKeysOfOtherVariants; nullFlattenedOpenapiKeys: typeof nullFlattenedOpenapiKeys; nullFlattenedOpenapiKeysOfOtherVariants: typeof nullFlattenedOpenapiKeysOfOtherVariants; serializerNameFromFullyQualifiedModelName: typeof serializerNameFromFullyQualifiedModelName; autogeneratedFileDisclaimer: typeof autogeneratedFileDisclaimer; }; /** * @internal * * Ensures that the application build is not missing any critical components * that would render it in an invalid state * */ private validateAppBuildIntegrity; /** * @internal * * Ensures that all models are in a valid state * */ private validateApplicationModels; /** * @internal * * */ private static setDatabaseTypeParsers; private static checkKey; /** * Returns the cached dream application if it has been set. * If it has not been set, an exception is raised. * * The dream application can be set by calling DreamApp#init, * or alternatively, if you are using Psychic along with Dream, * it can be set during PsychicApp#init, which will set caches * for both the dream and psychic applications at once. */ static getOrFail(): DreamApp; static log(...args: any[]): void; static logWithLevel(level: DreamLogLevel, ...args: any[]): void; private static loggerArgsToString; private _specialHooks; get specialHooks(): DreamAppSpecialHooks; private _dbCredentials; get dbCredentials(): Record; dbCredentialsFor(connectionName: string): DreamDbCredentialOptions | null; private _encryption; get encryption(): DreamAppEncryptionOptions; private _parallelTests; get parallelTests(): number | undefined; private _unicodeNormalization; get unicodeNormalization(): UnicodeNormalizationForm; private _paginationPageSize; get paginationPageSize(): number; private _paginationMaxPageSize; get paginationMaxPageSize(): number; /** * How long, in milliseconds, a position-mutating operation on a * `@deco.Sortable` model will wait for another transaction to release the * sort scope it needs before giving up with * `SortableScopeLockWaitTimedOut`. Applied to the lock acquisition statement * alone — the transaction's other lock waits keep whatever `lock_timeout` * the application has configured. * * `0` waits without bound, which is what Postgres's `lock_timeout` means by * it, and holds even on a connection configured with a `lock_timeout` of its * own: the acquisition disables the bound for the duration of the statement. * * Set it with `dreamApp.set('sortableScopeLockTimeout', milliseconds)`. */ private _sortableScopeLockTimeout; get sortableScopeLockTimeout(): number; private _primaryKeyType; get primaryKeyType(): LegacyCompatiblePrimaryKeyType; private _projectRoot; get projectRoot(): string; private _paths; get paths(): Required; private _serializerCasing; get serializerCasing(): DreamSerializerCasing; private _logger; get logger(): DreamLogger; private _inflections?; get inflections(): (() => void | Promise) | undefined; private _plugins; get plugins(): ((app: DreamApp) => void | Promise)[]; private _packageManager; get packageManager(): "pnpm" | "yarn" | "npm" | "bun" | "deno"; private _importExtension; get importExtension(): ".js" | ".ts" | "none"; /** * if set to true, it will bypass deprecation checks that run * during the sync hook. Defaults to false, we only recommend * overriding this if you are having issues with the deprecation * check. */ private _bypassDeprecationChecks; get bypassDeprecationChecks(): boolean; protected loadedModels: boolean; constructor(opts?: Partial); get models(): Record; get serializers(): Record; /** * The database name to connect to for a given connection. In dev/prod this is * simply the configured name. Under vitest (test env) it is the per-worker * database claimed from the pool — see {@link testDatabaseClaimEnabled} and * `src/db/testDatabasePool.ts`. * * This is synchronous because Kysely builds its `pg.Pool` synchronously. It * reads the already-claimed pool index; the claim is awaited earlier in the * worker — `DreamApp.init()` calls {@link claimTestDatabase} before the first * connection is built, and `dream-spec-helpers`' `truncate` awaits * {@link testDatabaseName} — so the index is always populated by the time any * connection resolves its name. */ dbName(connectionName: string, connection: DbConnectionType): string; /** * Single source of truth for the claimed test-database name, used both by * dream's connection resolution and by `dream-spec-helpers`' `truncate` * (which opens its own `pg.Client` and so must read — not recompute — the * claim). Awaits the pool claim if it has not happened yet in this worker. */ testDatabaseName(connectionName: string, connection: DbConnectionType): Promise; /** * Claim this worker's pool database if it hasn't been claimed yet. Idempotent * and a no-op outside the test-under-vitest path. Called by `DreamApp.init()` * before any connection is built so the synchronous {@link dbName} always * sees a populated index. */ ensureTestDatabaseClaimed(): Promise; dbConnectionConfig(connectionName: string, connection: DbConnectionType): DreamDbConfig; dbConnectionQueryDriverClass(connectionName: string): typeof QueryDriverBase; dbConnectionKeys(): string[]; hasReplicaConfig(connectionName: string): boolean; /** * Whether the runtime *requires* a per-worker test-database claim. Gated on * test env *under vitest* (not on `parallelTests > 1`): the slot-reuse * collision reproduces even serially, because vitest respawns a process per * file and the lingering process overlaps the new one on the same database. * CLI db tasks (`db:migrate`, `db:reset`, …) do not run under vitest, so they * keep the unsuffixed name and operate on the whole pool explicitly. * * This is independent of whether the default driver can actually claim — when * the need is present but the capability is absent, the claim path fails loud * (see {@link requireTestDatabaseClaimSupport}) rather than silently sharing * one database. */ get runtimeNeedsTestDatabaseClaim(): boolean; /** * Whether the default connection's query driver implements the per-worker * test-database claim seam (`openTestDatabaseLockSession`). The claim is keyed * off the **default** connection's capability so a Postgres-default app with a * MySQL secondary keeps working (the secondary borrows the Postgres-claimed * index for its database name). */ private get defaultConnectionSupportsParallelTestDatabases(); /** * Whether the per-worker test-database pool claim is active: needed by the * runtime *and* supported by the default connection's driver. Consumers that * key behavior off the claimed index (connection-cache suffixing) use this; * the claim entry points additionally fail loud when the need is present but * support is absent. */ get testDatabaseClaimEnabled(): boolean; /** * Throw an actionable error when the runtime needs a per-worker test-database * claim but the default connection's driver cannot provide one — rather than * silently sharing a single database across overlapping vitest workers (which * reintroduces the flake this whole mechanism exists to fix). */ private requireTestDatabaseClaimSupport; load(resourceType: RT, resourcePath: string, importCb: (path: string) => Promise): Promise; plugin(cb: (app: DreamApp) => void | Promise): void; set(applyOption: ApplyOpt, options: ApplyOpt extends 'bypassDeprecationChecks' ? boolean : ApplyOpt extends 'db' ? DreamDbCredentialOptions | string : ApplyOpt extends 'encryption' ? DreamAppEncryptionOptions : ApplyOpt extends 'primaryKeyType' ? LegacyCompatiblePrimaryKeyType : ApplyOpt extends 'importExtension' ? GeneratorImportStyle : ApplyOpt extends 'logger' ? DreamLogger : ApplyOpt extends 'projectRoot' ? string : ApplyOpt extends 'inflections' ? () => void | Promise : ApplyOpt extends 'packageManager' ? DreamAppAllowedPackageManagersEnum : ApplyOpt extends 'paths' ? DreamDirectoryPaths : ApplyOpt extends 'parallelTests' ? number : ApplyOpt extends 'unicodeNormalization' ? UnicodeNormalizationForm : ApplyOpt extends 'paginationPageSize' ? number : ApplyOpt extends 'paginationMaxPageSize' ? number : ApplyOpt extends 'sortableScopeLockTimeout' ? number : never, secondaryOptions?: ApplyOpt extends 'db' ? DreamDbCredentialOptions : never): void; /** * Registers a callback for an app-level event. Callbacks for the same * event run in the order they were registered. * * - `'db:log'`: called with a {@link KyselyLogEvent} each time a query on a * Dream database connection completes or fails. * - `'repl:start'`: called, and awaited, with the console's REPL context * when the Dream console starts. * - `'encryption:legacy-key-used'`: called with an * {@link EncryptionLegacyKeyUsedEvent} when reading an `@deco.Encrypted` * property needed the `legacy` column encryption key, not `current`, to * open the stored value. Use it during a key rotation to find rows still * encrypted with the old key. The property decrypts on every read, so a * record whose value is still under the old key fires the event each time * the property is read. A row nobody reads never fires it, so a quiet * event does not prove that `legacy` is safe to drop; * `Model.reencryptEncryptedColumns()` rewrites every row instead. Neither * it nor the migration helpers fire the event. The callback runs * synchronously inside the property read, and an error it throws * propagates from that read. Reading the same property of the same * record inside the callback returns its value without firing the event * again, so a callback can read the property and assign the value back, * which re-encrypts it under `current` for the record's next save. That * holds only while the callback runs synchronously: a callback that * reads the property after an `await` fires the event again from that * read, so an `async` callback should read the property before its * first `await`. * * ```ts * dreamApp.on('encryption:legacy-key-used', ({ dream, property }) => { * console.warn(`${dream.constructor.name} ${dream.primaryKeyValue()} ${property} still uses the legacy key`) * }) * ``` */ on(hookEventType: T, cb: T extends 'db:log' ? (event: KyselyLogEvent) => void : T extends 'repl:start' ? (context: Context) => void | Promise : T extends 'encryption:legacy-key-used' ? (event: EncryptionLegacyKeyUsedEvent) => void : never): void; } export type DreamHookEventType = 'db:log' | 'repl:start' | 'encryption:legacy-key-used'; export interface DreamAppOpts { projectRoot: string; primaryKeyType: LegacyCompatiblePrimaryKeyType; db: DreamDbCredentialOptions; inflections?: () => void | Promise; paths?: DreamDirectoryPaths; serializerCasing?: DreamSerializerCasing; parallelTests: number | undefined; } export type DreamAppSetOption = 'bypassDeprecationChecks' | 'db' | 'encryption' | 'inflections' | 'importExtension' | 'logger' | 'paths' | 'primaryKeyType' | 'projectRoot' | 'serializerCasing' | 'parallelTests' | 'unicodeNormalization' | 'paginationPageSize' | 'paginationMaxPageSize' | 'sortableScopeLockTimeout' | 'packageManager'; export interface DreamDirectoryPaths { models?: string; serializers?: string; conf?: string; db?: string; modelSpecs?: string; factories?: string; types?: string; } export interface DreamDbCredentialOptions { primary: DreamDbConfig; replica?: DreamDbConfig | undefined; queryDriverClass?: typeof QueryDriverBase; /** * The tables `sync` writes types for, when it should not be every table * the connection can read. Every table it writes types for needs a Dream * model. A table name, or a glob in Node's `path.matchesGlob` syntax, * compared without regard to case, e.g. * "users" * "chalupas*" * "public.*" * "public.+(users|posts)" * * A pattern that contains a `.` is matched against `.`, and * any other against the table's name alone. */ tableIncludePattern?: string; /** * The tables `sync` leaves out of the types it writes, e.g. tables that no * Dream model reads. Written like `tableIncludePattern`, and applied after * it. */ tableExcludePattern?: string; } type UnicodeNormalizationForm = 'NFC' | 'NFD' | 'none'; export interface DreamDbConfig { user: string; /** * A fixed password, or a provider for Dream's built-in PostgreSQL driver. * PostgreSQL calls the provider when the server requests password * authentication for a new physical connection. The provider may return a * string or a promise of one. Each such connection gets a fresh result; * existing pooled connections keep their credential. A rejected provider * fails that connection attempt and reaches its caller; Dream does not cache * credentials or retry. Custom query drivers receive the configured value * and must handle or reject a provider themselves. */ password: string | (() => string | Promise); host: string; name: string; port: number; /** * @deprecated Use `ssl` instead. * * Legacy boolean opt-in for Postgres TLS. When `true` (and `ssl` is not * set), Dream connects with `{ rejectUnauthorized: false }` — TLS is on but * the server certificate is not verified. Preserved for back-compat and * will be removed in a future major version. New code should set `ssl` * directly. */ useSsl?: boolean; /** * TLS configuration passed straight through to `pg.Pool`'s `ssl` field. * Takes precedence over the deprecated `useSsl` when provided. * * Set `rejectUnauthorized: true` (Node's own default) for verified TLS * against the system CA store — the right choice for managed providers that * present a public-CA-signed certificate (Supabase, Neon, Render, Azure * Database for PostgreSQL on Flexible Server, etc.). * * For providers that present a private-CA certificate (AWS RDS, * GCP Cloud SQL), add a `ca` bundle: * `ssl: { rejectUnauthorized: true, ca: readFileSync('rds-ca.pem') }`. * * For providers that present a self-signed certificate (Heroku Hobby, * some local docker images), set `rejectUnauthorized: false` — encrypted * but unauthenticated. * * Set `false` to disable TLS entirely. Omitting `ssl` (and `useSsl`) throws * at `app.set('db', ...)` time so the safety question is a deliberate * decision at the call site rather than a silent default. */ ssl?: TlsConnectionOptions | false; /** * pg pool/client options passed straight through to `new pg.Pool(...)`. * Dream knows nothing about these fields — pg's own types carry the * documentation. Unset ⇒ pg applies its own defaults (backward compatible). * * Omitted from the passthrough: * - `user / password / database / host / port / ssl`: Dream manages these * (per-connection name, TLS directive) — hard invariants. * - `connectionString`: `pg`'s `ConnectionParameters` re-parses the URL * and lets its fields take precedence, bypassing Dream's per-connection * database name and TLS directive. Parse `DATABASE_URL` into the discrete * `host/port/user/password/name/ssl` fields in `conf/dream.ts` instead. * - `min`: node-pg's `pg-pool` does not honor it (silent no-op). * - `types / Client / Promise / log / stream`: programmatic, not * credential config (`types` is already wired via Dream's parsers). * * When another database adapter is added, a parallel `mysql?: ...` key * (or similar) will appear here — this key is intentionally pg-specific. */ pg?: Omit; } export type DreamLogger = { info: (...args: any[]) => void; warn: (...args: any[]) => void; debug: (...args: any[]) => void; error: (...args: any[]) => void; }; export type DreamLogLevel = 'info' | 'warn' | 'debug' | 'error'; export type DreamSerializerCasing = 'snake' | 'camel'; export interface DreamAppEncryptionOptions { columns: SegmentedEncryptionOptions; } interface SegmentedEncryptionOptions { current: EncryptOptions; legacy?: EncryptOptions; } export interface DreamAppSpecialHooks { dbLog: ((event: KyselyLogEvent) => void)[]; replStart: ((context: Context) => void | Promise)[]; encryptionLegacyKeyUsed: ((event: EncryptionLegacyKeyUsedEvent) => void)[]; } /** * Passed to `dreamApp.on('encryption:legacy-key-used', ...)` callbacks. */ export interface EncryptionLegacyKeyUsedEvent { /** * The record whose `@deco.Encrypted` property was read. Its class and * `primaryKeyValue()` identify the row; a record not yet saved has no * primary key. */ dream: Dream; /** * The `@deco.Encrypted` property that was read, e.g. `'phone'`. */ property: string; /** * The column holding the property's ciphertext, e.g. `'encryptedPhone'`. */ encryptedColumnName: string; } export interface DreamAppInitOptions { bypassDreamIntegrityChecks?: boolean; bypassDbConnectionsDuringInit?: boolean; } export interface KyselyLogEvent { level: 'query' | 'error'; query: CompiledQuery; queryDurationMillis: number; error: unknown; } export declare const DreamAppAllowedPackageManagersEnumValues: readonly ["pnpm", "yarn", "npm", "bun", "deno"]; export type DreamAppAllowedPackageManagersEnum = (typeof DreamAppAllowedPackageManagersEnumValues)[number]; export declare const GeneratorImportStyles: readonly [".js", ".ts", "none"]; export type GeneratorImportStyle = (typeof GeneratorImportStyles)[number]; export {};