Skip to main content

Database

flecks provides database connection through Sequelize and database server instances through either flat SQLite databases or Docker-ized database servers.

Install and configure

We'll start from scratch as an example. Create a new flecks application:

npm init @flecks/app db-test

Now in your new application directory, add @flecks/db:

npx flecks add @flecks/db

Finally,

npm run start

You will see lines like the following in the logs:

  @flecks/db/server/connection config: { dialect: 'sqlite', storage: ':memory:' } +0ms
@flecks/db/server/connection synchronizing... +107ms
@flecks/db/server/connection synchronized +2ms

By default, flecks will connect to an in-memory SQLite database to get you started instantly.

Your models

Astute observers may have noticed a line preceding the ones earlier:

  @flecks/core/flecks gathered '@flecks/db.models': [] +0ms

Let's create a fleck that makes a model so we can get a feel for how it works.

First, create a fleck in your application:

npm init @flecks/fleck -w packages/content

Now, let's hop into packages/content/src/index.js and add a hook implementation:

packages/content/src/index.js
export const hooks = {
'@flecks/db.models': (flecks) => {
const {Model, Types} = flecks.fleck('@flecks/db/server');
class Content extends Model {

static get attributes() {
return {
text: {
type: Types.TEXT,
allowNull: false,
},
};
}

}
return {
Content,
};
},
};

Save the file and the server will immediately restart. You will see that line looks different now:

  @flecks/core/flecks gathered '@flecks/db.models': [ 'Content' ] +0ms

Our model is recognized!

Let's add one more model and create an association between them:

packages/content/src/index.js
export const hooks = {
'@flecks/db.models': (flecks) => {
const {Model, Types} = flecks.fleck('@flecks/db/server');
class Content extends Model {

static get attributes() {
return {
text: {
type: Types.TEXT,
allowNull: false,
},
};
}

static associate({Tag}) {
this.hasMany(Tag);
}

}
class Tag extends Model {

static get attributes() {
return {
value: {
type: Types.STRING,
allowNull: false,
},
};
}

static associate({Content}) {
this.belongsTo(Content);
}

}
return {
Content,
Tag,
};
},
};

This time our line in the logs looks like:

  @flecks/core/flecks updating gathered @flecks/db.models from @db-test/content with [ 'Content', 'Tag' ] +5ms
Ess-Cue-Ell-ize

@flecks/db uses Sequelize under the hood. You can dive into their documentation to learn even more.

Providing models with Flecks.provide()

When building Real™ applications we are usually going to need a bunch of models. If we add all of them into that one single file, things are going to start getting unwieldy. Let's create a src/models directory in our packages/content fleck and add a content.js and tag.js source files with the following code:

packages/content/src/models/content.js
export default (flecks) => {
const {Model, Types} = flecks.fleck('@flecks/db/server');
return class Content extends Model {

static get attributes() {
return {
text: {
type: Types.TEXT,
allowNull: false,
},
};
}

static associate({Tag}) {
this.hasMany(Tag);
}

};
};
packages/content/src/models/tag.js
export default (flecks) => {
const {Model, Types} = flecks.fleck('@flecks/db/server');
return class Tag extends Model {

static get attributes() {
return {
value: {
type: Types.STRING,
allowNull: false,
},
};
}

static associate({Content}) {
this.belongsTo(Content);
}

};

};

Notice that this looks very similar to how we defined the models above, but this time we're only returning the classes.

Now, hop over to packages/content/src/index.js and let's rewrite the hook implementation:

packages/content/src/index.js
import {Flecks} from '@flecks/core';

export const hooks = {
'@flecks/db.models': Flecks.provide(require.context('./models')),
};
This kills the webpack

If your application has been running and smoothly updating with all the changes, it will be killed once you add that require.context line when it tries to reload. This is a limitation of webpack.

You'll need to restart your application. When webpack errors out, flecks will not automatically restart your application to prevent an infinite failure loop.

We're passing the path to our models directory to require.context which is then passed to Flecks.provide. This is completely equivalent to our original code, but now we can add more models by adding individual files in packages/content/src/models and keep things tidy.

Continue gathering knowledge

For a more detailed treatment of gathering and providing in flecks, see the gathering guide.

Working with models

Let's do something with them. Edit packages/content/src/index.js again like so:

packages/content/src/index.js
import {Flecks} from '@flecks/core';

export const hooks = {
'@flecks/server.up': Flecks.priority(
async (flecks) => {
const {Content, Tag} = flecks.db.Models;
console.log(
'There were',
await Content.count(),
'pieces of content',
'and',
await Tag.count(),
'tags.',
);
},
{after: '@flecks/db/server'},
),
'@flecks/db.models': Flecks.provide(require.context('./models')),
};

We use Flecks.priority so that the database comes up before we try to use it.

You will find this line in the output:

There were 0 pieces of content and 0 tags.

Not very interesting. Let's add some, but only if there aren't any yet:

packages/content/src/index.js
import {Flecks} from '@flecks/core';

export const hooks = {
'@flecks/server.up': async (flecks) => {
const {Content, Tag} = flecks.db.Models;
console.log(
'There were',
await Content.count(),
'pieces of content',
'and',
await Tag.count(),
'tags.',
);
if (0 === await Content.count()) {
await Content.create(
{text: 'lorem ipsum', Tags: [{value: 'cool'}, {value: 'trending'}]},
{include: [Tag]},
);
await Content.create(
{text: 'blah blah', Tags: [{value: 'awesome'}]},
{include: [Tag]},
);
}
console.log(
'There are',
await Content.count(),
'pieces of content',
'and',
await Tag.count(),
'tags.',
);
},
'@flecks/db.models': Flecks.provide(require.context('./models')),
};

You will see:

There were 0 pieces of content and 0 tags.
There are 2 pieces of content and 3 tags.

Great!

Persistence

You'll might that if you stop the server and start it again, it will always say:

There were 0 pieces of content and 0 tags.
There are 2 pieces of content and 3 tags.

What's up with that? Remember in the beginning:

By default, flecks will connect to an in-memory SQLite database to get you started instantly.

This means that the database will only persist as long as the life of your application. When you restart it, you'll get a fresh new database every time. It was quick to get started developing, but this isn't very helpful for any real purpose. Let's make a change to our build/flecks.yml:

build/flecks.yml
'@db-test/content:./packages/content': {}
'@flecks/core':
id: db-test
'@flecks/db':
database: './persistent.sql'
'@flecks/server': {}

Kill and restart your application and you'll see our old familiar message:

There were 0 pieces of content and 0 tags.
There are 2 pieces of content and 3 tags.

This time though, our application wrote the SQLite database to disk at ./persistent.sql. If we give it one more go, we'll finally see what we expect:

There were 2 pieces of content and 3 tags.
There are 2 pieces of content and 3 tags.

A persistent database!

Containerization

Sure, our database is persistent... kinda. That persistent.sql file is a bit of a kludge and isn't much of a long-term (or production) solution.

flecks's small-core philosophy means that you don't pay for spinning up a database by default. However, it's trivial to accomplish a "real" database connection if you have Docker installed on your machine.

Let's add another fleck to our project:

npx flecks add -d @flecks/docker

Configure build/flecks.yml:

build/flecks.yml
'@db-test/content:./packages/content': {}
'@flecks/core':
id: db-test
'@flecks/db':
database: db
dialect: postgres
password: THIS_PASSWORD_IS_UNSAFE
username: postgres
'@flecks/docker': {}
'@flecks/server': {}

Add pg to connect to postgres:

npm install pg

Now,

Start your application and you will see some new lines in the logs:

  @flecks/docker/container creating datadir '/tmp/flecks/flecks/docker/sequelize' +0ms
@flecks/docker/container launching: docker run --name flecks_sequelize -d --rm -p 5432:5432 -e POSTGRES_USER=... -e POSTGRES_DB=... -e POSTGRES_PASSWORD=... -v /tmp/flecks/flecks/docker/sequelize:/var/lib/postgresql/data postgres +0ms
@flecks/docker/container 'sequelize' started +372ms
@flecks/db/server/connection config: { database: 'db', dialect: 'postgres', host: undefined, password: '*** REDACTED ***', port: undefined, username: 'postgres' } +0ms
@flecks/db/server/connection synchronizing... +2s
@flecks/db/server/connection synchronized +3ms

and of course, we see:

There were 0 tags.
There are 2 tags.

because we just created a new postgres database from scratch just then! Kill the application and restart and you will see what you expect:

There were 2 tags.
There are 2 tags.

Awesome, we have a connection to a real postgres database!

Production

Sure, spinning up a database like magic is spiffy for development, but you probably want to be a little less freewheeling on your production server.

Build the application we've written so far:

npm run build

Then, take a look in the dist/server directory. You'll see a file there called docker-compose.yml. @flecks/docker automatically emits this file when you build your application for production to make container orchestration easier. Let's take a look:

dist/server/docker-compose.yml
version: '3'
services:
db-test_app:
build:
context: ../..
dockerfile: dist/server/Dockerfile
environment:
FLECKS_ENV__flecks_docker__enabled: 'false'
FLECKS_ENV__flecks_db__host: sequelize
volumes:
- ../../node_modules:/var/www/node_modules
sequelize:
image: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_DB: db
POSTGRES_PASSWORD: THIS_PASSWORD_IS_UNSAFE

Notice our database container is included and already prepopulated with the configuration we specified!

You can run (after you install Docker Compose if necessary):

docker-compose -f dist/server/docker-compose.yml up --build

This demonstrates that your application is now being orchestrated by Docker Compose and is chugging right along!