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
npm init @flecks/app db-test
Now in your new application directory, add @flecks/db
:
- npm
npx flecks add @flecks/db
Finally,
- npm
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
npm init @flecks/fleck -w packages/content
Now, let's hop into packages/content/src/index.js
and add a hook implementation:
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:
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
@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:
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);
}
};
};
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:
import {Flecks} from '@flecks/core';
export const hooks = {
'@flecks/db.models': Flecks.provide(require.context('./models')),
};
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.
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:
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:
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
:
'@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:
- npm
npx flecks add -d @flecks/docker
Configure 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
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
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:
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!