dream
Since dream is an independent library that is imported by psychic, we expose configuration for you which enables dream to understand, for example, what your database credentials are, or where you want your database migrations to go. These configuration items are set in the conf/dream.ts
file, which we will be covering in depth in this guide.
conf/dream.ts
A configuration file is used to configure dream, and can be found at the conf/dream.ts
location. This file is concerned with provisioning your dream application with database credentials, primary key types, etc...
database connection
In order for Dream to connect to your database, you need to provide it with explicit credentials. Dream supports both primary and replica db connections, and configuration for both can be provided. While this example taps into env vars, we recommend that in production, you switch to something more secure, like AWS Secrets Manager.
export default async function (dream: DreamApp) {
dream.set('db', {
primary: {
user: AppEnv.string('DB_USER'),
password: AppEnv.string('DB_PASSWORD'),
host: AppEnv.string('PRIMARY_DB_HOST'),
name: AppEnv.string('PRIMARY_DB_NAME'),
port: parseInt(AppEnv.integer('DB_PORT')),
// only connect to replica db insecurely if `DB_NO_SSL` is explicitly set
useSsl: !AppEnv.boolean('DB_NO_SSL'),
},
replica: {
user: AppEnv.string('DB_USER'),
password: AppEnv.string('DB_PASSWORD'),
host: AppEnv.string('REPLICA_DB_HOST'),
name: AppEnv.string('REPLICA_DB_NAME'),
port: parseInt(AppEnv.integer('DB_PORT')),
// only connect to replica db insecurely if `DB_NO_SSL` is explicitly set
useSsl: !AppEnv.boolean('DB_NO_SSL'),
},
}),
...
}
leveraging multiple database connections simultaneously
Dream provides the ability for you to house multiple database connections within your application. To do this, simply call the set
method a second time, this time providing it with a string argument for a connectionName before providing it your connection options.
dream.set('db', 'myAlternateConnection', {
primary: ...,
})
Be sure to add any new environment variables to your .env and .env.test files.
- Run sync
yarn psy sync
- add a new application model for your new connection, naming it the name of your connection, pascalized, with the string
ApplicationModel
at the end, like so:
// app/models/MyAlternateConnectionApplicationModel.ts
import Dream from '../../../src/Dream.js'
import { DBClass } from '../../types/db.alternateConnection.js'
import { globalSchema, schema } from '../../types/dream.alternateConnection.js'
export default class MyAlternateConnectionApplicationModel extends Dream {
public declare DB: DBClass
public override get connectionName() {
return 'myAlternateConnection' as const
}
public override get schema(): any {
return schema
}
public override get globalSchema() {
return globalSchema
}
}
- Now you can proceed to generate a model for your new connection, like so:
yarn psy g:model MyNewModel someField:text --connection-name=myAlternateConnection
Dream will automatically read the connectionName and use it to derive the MyAlternateConnectionApplicationModel
automatically, though if this isn't correct, you will need to manually adjust it.
Any migrations for other models will go into a special folder named after the connection, i.e. src/db/migrations/myConnectionName/
, so that they can be isolated from those for the default connection. Whenever running migrations, dream will run them for both connections.
Any models using a separate db connection will be isolated in scope, and cannot be joined or associated with other models in your system unless they are using the same db connection.
providing a custom query driver
By default, dream is strapped to a postgres driver, and dream does not inherently offer any other drivers for performing query execution. However, if you are determined to make dream work with a different database engine, it is possible for you to provide a custom query driver, which you can provide as part of the db configuration, like so:
// src/conf/dream.ts
app.set('db', {
queryDriverClass: MyCustomQueryDriver,
...,
})
This can be done on a connection-to-connection basis, meaning you can have one connection use postgres, and another use a custom driver, like so:
app.set('db', {
...,
})
app.set('db', 'myMysqlConnection' {
queryDriverClass: MyCustomMysqlDriver,
...,
})
Your custom query driver class will need to extend the QueryDriverBase
class. This class is responsible for handling any part of the dream ecosystem that makes a call to the database. This includes the migration engine, the query engine underlying all dream models, the helper functions used to provision the database, introspect the database to build types, etc...
If you are looking to build an adapter to extend one of the existing kysely drivers, we recommend you try extending the KyselyQueryDriver
class instead, since it is basically set up to do this. For an example, you can take a look at this hypothetical mysql driver, which was built to test out the ability to integrate custom drivers, should anyone be interested in taking on the endeavor. Be warned, there be dragons in that adapter, it was not built for production-grade use, so tread lightly. An entire function was written by ai without much vetting, and should probably be entirely re-written. The rest of it was written as a proof of concept, and hardly any testing around it, so only use it as a reference.
loading your application files
In order for dream to function properly, it needs you to provide it folders to scan to locate:
- your models
- your serializers
- your services
To do so, every dream application must have the following lines provided in the dream configuration:
export default async function (dream: DreamApp) {
app.load('models', srcPath('app', 'models'), await importModels())
app.load('serializers', srcPath('app', 'serializers'), await importSerializers())
app.load('services', srcPath('app', 'services'), await importServices())
...
}
setting your project root path
Dream requires you to provide it the path from your configuration file to the project's root file. This should be an absolute path, including the __dirname variable provided by node.
export default async function (dream: DreamApp) {
dream.set('projectRoot', path.join(__dirname, '..', '..', '..'))
...
}
setting your primary key type
The primaryKeyType configuration informs the cli how to generate your migrations for you when building out models using the cli generators provided by dream.
export default async function (dream: DreamApp) {
dream.set('primaryKeyType', 'bigserial')
...
}
providing inflections
Provide inflections to your dream application to inform the dream cli generators how to generate pluralized version of singulars.
import inflections from './inflections'
export default async function (dream: DreamApp) {
dream.set('inflections', inflections)
...
}
application logging
Provide inflections to your dream application to inform the dream cli generators how to generate pluralized version of singulars.
// conf/dream.ts
import inflections from './inflections'
export default async function (dream: DreamApp) {
dream.set(
'logger',
winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'user-service' },
transports: [
//
// - Write all logs with importance level of `error` or less to `error.log`
// - Write all logs with importance level of `info` or less to `combined.log`
//
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
}),
)
...
}
If no logger is provided, dream will log using console.log
, which is not recommended, since console.log
is blocking.
paths
Provide information about the paths to the specific directories that dream cares about. These are only used by the cli locally to generate new files into your application, and it is optional to pass them. By default, they will use the typical paths of a psychic application to generate your files.
// conf/dream.ts
export default async function (dream: DreamApp) {
// provides a list of path overrides for your app. This is optional, and will default
// to the paths expected for a typical psychic application.
dream.set('paths', {
conf: 'my/custom/path/to/app/conf',
db: 'my/custom/path/to/db',
factories: 'my/custom/path/to/factories',
models: 'my/custom/path/to/app/models',
serializers: 'my/custom/path/to/app/serializers',
services: 'my/custom/path/to/app/services',
modelSpecs: 'my/custom/path/to/spec/unit/models',
})
...
}
parallelTests
Dream tests can be slower than traditional vitest testing, since each test utilizes a connection to a single database, and that database must be truncated between each test run, preventing multiple test runs from being done at once. This means your tests must run one at a time, which can be quite slow. To speed things up, we provide the ability for you to partition your test runs onto multiple databases, enabling you to run as many tests as you have databases at once.
export default async function (dream: DreamApp) {
app.set('parallelTests', AppEnv.integer('DREAM_PARALLEL_TESTS', { optional: true }) || 1)
...
}
The value provided to parallelTests
needs to be an integer, and will be used to determine how many copies of your database to make. It will then ensure that each test run gets a unique database which will be left untampered by other parallel test runs.
ApplicationModel
Additionally, a base model (called ApplicationModel
) is pre-established for you which contains carefully constructed type bindings to bridge the types generated by kysely throughout our app through each of your models. This happens automatically for you, but it must be the base class for each of your models in order for the type safety of our system to work properly.
import { Dream, Dreamconf } from '@rvoh/dream'
import { AllColumns, DBClass } from '../../types/db'
import { schema } from '../../types/dream'
import dreamconf from '../../conf/dreamconf'
export default class ApplicationModel extends Dream {
public declare DB: DBClass
public get allColumns(): typeof AllColumns {
return AllColumns
}
public get dreamconf(): Dreamconf<DBClass, typeof schema> {
return dreamconf
}
}