npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@fehujs/database

v0.1.2

Published

Fehujs database package

Downloads

204

Readme

@fehujs/database

This module permits you to interact with a database, to handle your migrations and your models.

For now on, only the SQLite database provider is implemented, but you can implement other providers on your own using the API (more info at the bottom of this file).

It uses Knex under the hood, but maybe I'll create a homemade SQL query builder (one day).

Please note that foreign keys aren't planned to be implemented. If you want to link two tables create a field in the first one that contains the second table primary key.

DISCLAIMER: some features are not fully available or tested (alter table, findByWithSQL, SQL injection avoider), please consider test them yourselves before using these features.

Note: you can install the SQLite Viewer VS code extension (from Florian Klampfer) to view your database directly from the IDE.

Start

Create a migration and then run it (more infos at the next section). And run it.

The default database provider is SQLite, but you can choose to use PostgreSQL provider if you want by updating the app's config (change the PROVIDER and CONFIG).

You can edit the name of the database file by setting a DATABASE_NAME in your .env file.

Migrations

The migrations can help you to define the tables of your database.

To use them, you'll need to create a directory (if not already created) src/app/migrations, with in it, an index.ts file.

import { BaseMigration } from "#database"

import { AddPostMigration } from "./add_post"


const migrations: any = {
    'add_post': new AddPostMigration(),
}

Object.keys(migrations).forEach(key => BaseMigration.runMigration(key, migrations[key]))

Create a migration

You have to create a new file in the migrations directory. For this example I will create a Post migration.

So, here's my migration:

import { BaseMigration, provider, Table } from "#database"

export class AddPostMigration extends BaseMigration {
    protected table = {
        name: "posts",
        columns: [
        ]
    }

    public async up() {
    }

    public async down() {
    }
}

Define tables

To define tables, you have to put columns into your table. Watch out:

// ...

export class AddPostMigration extends BaseMigration {
    protected table = {
        name: "posts",
        columns: [
            {
                name: 'id',
                type: 'INTEGER',
                isNotNull: true,
                isPrimaryKey: true,
                isUnique: true,
                // isAutoincrement: true  // Nota: I won't use autoincrement to handle ids in this example
            },
            {
                name: 'title',
                type: 'TEXT',
                isNotNull: true
            },
            {
                name: 'content',
                type: 'TEXT',
                isNotNull: true,
            }
        ]
    }

    // ...

Nota: here's the column typing:

type Column = {
    name: string
    /** type of the contained value */
    type: 'TEXT' | 'INTEGER' | 'NULL' | 'REAL' | 'BLOB'
    default?: string
    isAutoIncrement?: boolean
    isNotNull?: boolean
    isPrimaryKey?: boolean
    isUnique?: boolean
}

Don't forget to create the up() and down() methods, the first one to set the migration up, and the second to cancel it.

    public async up() {
        provider.createTable(this.table)
    }
    
    public async down() {
        provider.dropTable(this.tableName)
    }

Drop

You can drop a table using provider.dropTable(this.tableName).

Run migrations

Now we finished implementing your migration, we want to run it.

So we import our migration into src/app/migrations/index.ts and we create a new instance of the migration class in the migrations object:

//...
import { AddPostMigration } from "./add_post"

const migrations: any = {
    'add_post': new AddPostMigration()
}
//...

Then, we need to run the migration, so we open our project in the console, and we run the command 'migrate' from package.json. It will create the database file and run the migration after.

Nota: flags.

You can specify the migration key (from the migrations object from src/app/migrations/index.ts), and set it on up if you want to run this migration.

Example (in this case): pnpm migrate add_post=up, but you can put add_post=down. If it isn't specified, nothing happends.

If you don't put any flag, all registred migrations that were not runned (their names aren't situated in the migrations.json at the root of the project) will be runned (mode up). If you specify a key, only the migration.s specified will be executed.

Alter table

This isn't fully tested, so please do some tests before using these features.

You can look at the code if you want more informations about it.

Models

You can see the models as a database handler of datas. In other words, the model isn't a data container, but an interface between database and main code, you'll see what I want to say.

Nota: all the methods are static and async.

Create a model

You need to create a directory named src/app/models and you create a new file containing your model in it.

Example:

import { BaseModel } from "#database"

import { AddPostMigration } from "../migrations/add_post"

export class Post extends BaseModel {
    public static table = (new AddPostMigration()).getTable()  // this is to avoid the rewriting of the table

    // if your (main) private key isn't 'id', change it there
    protected idCol: string = 'your_main_private_key_column_name'
    
    // don't forget to declare the fields in the model
    declare id: string

    declare title: string

    declare content: string
}

Please note that you can override every method of BaseModel if you need something more complex (you can see an example in the Auth module).

Get

There's many ways to get datas from database with this system.

Model.find(id: string | number): Promise (static)

You can get an item with its primary key.

Model.findBy(key: string, value: string, operator: Operator = '='): Promise<BaseModel[]> (static)

You can get many items using this function (key param stands for the column name, the value stands for the value to be compared, and the operator stands for the operation between key and the value)

Model.findWithSql(condition: string): Promise<BaseModel[]> (static) (depreciated)

Supposed to return an array of models selected with a SQL selection, condition.

WARNING: escaping not provided. Consider this feature as experimental.

Warning: depreciated. Instead use provider.query() (returns an instance of Knex).

Model.findAll(): Promise<BaseModel[]> (static)

Returns all items of a table.

Create

Model.create(options: ModelObject): Promise (static)

Creates a new object in the database and returns it.

Edit/save

model.save(): Promise

Let's have a demo:

    const post = new Post()
    post.id = "...."
    post.title = "hello there"
    post.content = "."

    // right now my model ("post") isn't stocked in the db

    post.save()

    // now it is

    post.content = "new content" // the update isn't stocked in the db, but the new value is stocked in the model's data

    post.save() // the changes have been stocked in the db

Delete

model.destroy(): Promise

Destroys the item specified.

Format data

model.toObject(): ModelObject

Returns the model data stocked in model.data, not the data from db (the difference is that the model.data may be different than db because it stores values updates before saving in the db).

model.toJson(): string

Returns the model data stocked in model.data as JSON.

Serializing

model.serialize(fields: Serialize): ModelObject | undefined

Serializes the model according to provided fields.

This is the typing of fields:

type Serialize = {
    [keys: string]: {
        serializeAs?: string
        doSerialize?: (value: string | number) => string
    }
}

There's a little example:

const posts = await Post.findAll() as Post[]

const postsSerialized: ModelObject[] = await new Promise((resolve, reject) => {
    let serialized: ModelObject[] = []

    posts.forEach(async post => {
        const postSerialized = post.serialize({
            id: {
                serializeAs: "postId"
            },
            title: {
                doSerialize: (value: string | number) => `${value.toString().slice(0, 10)}...`
            }
        })
        serialized.push(postSerialized!)
    })
    
    resolve(serialized)
})

Seeders

You can create seeders if you want to create items automatically.

Let's create a file post.ts in the directory named src/app/seeders.

import { Post } from "../models/post"
import { BaseSeeder } from "#database"


export class PostSeeder extends BaseSeeder {
    public async run() {
        await Post.create({
            id: Date.now(),
            title: "created by seeder",
            content: "this is a post created by the seeder"
        })
    }
}

Now, register the seeder (as for migrations):

import { BaseSeeder } from "#database"

import { PostSeeder } from "./post"


const seeders: { [key: string]: BaseSeeder } = {
    "create_initial_post": new PostSeeder()
}

Object.keys(seeders).forEach(key => BaseSeeder.runSeeder(key, seeders[key]))

Then run pnpm run run:seeders create_initial_post.

Nota: you must specify the seeder key to run the seeder.

Usage of the providers' API

You might need to use the providers' API without models to create complex requests to your database.

The databases providers are implementing DatabaseProviderInterface:

connectDb(dbPath: string): void

Connect the DB for interaction.

closeConnection(): void

Closes the connection after transaction.

get db(): any | undefined

Returns the DB object.

get dbPath(): string

Returns the DB path.

createTable: (table: CreateTable) => void

Permits to create a new table.

alterTable: (table: AlterTable) => void

Permits to alter a table, usefull for migrations.

Warning: not implemented yet.

dropTable: (table: Table) => void

Drops a table.

async query(sql: string, params?: string[])

It permits to run a query by specifying a raw SQL query to your DB.

select (table: Table, condition?: any): Promise<T[]>

Runs a select query.

Nota: if you want to select all columns, don't set the them on the table argument.

Example:

We have a posts table with these columns: id, title, content.

If we want to get all these columns, you can do :

    const posts = await provider.select((new AddPostsMigration()).getTable(), "..") // get the cols directly specifying the table

    // OR

    const posts = await provider.select({ name: "posts" }, "...")  // create a "new table" with only the name

insert (table: Table, value: T): Promise

Runs an insert query.

update (table: Table, condition: any, value: T): Promise

Runs an update query.

Nota: same remark as select.

delete (table: Table, condition: any): Promise<T[]>

Runs an delete query.

Nota: same remark as select and update.