Search code examples
javascriptknex.jsfeathersjs

How to load initial data in a Database table in feathers.js


Here's a problem I'm banging my head around since months now:

I'm searching for an elegant,consistent way to initialize a table, the first time it is created, whithin feathers-knex

I'm using feathers.js (5.0.5) + feathers-knex (8.0.1) + knex (2.4.2) Note that I came to my solution to my problem with version 4 of feathers already

I have a service generated from the provided generators:

...
const { Rootdir } = require('./rootdir.class');
const createModel = require('../../models/rootdir.model');
const hooks = require('./rootdir.hooks');

module.exports = function (app) {
  const options = {
    // id: 'id', // Optional
    Model: createModel(app),
    paginate: app.get('paginate')
  };
  app.use('/rootdir', new Rootdir(options, app));
  const service = app.service('rootdir');
  service.hooks(hooks);
}

where rootdir.model.js looks something like:

module.exports = function (app) {
  const db = app.get('knexClient');
  const tableName = 'rootdir';
  db.schema.hasTable(tableName).then(exists => {
    if (!exists) {
      db.schema.createTable(tableName, table => {
        table.string('id').primary();
        table.string('dir');
      })
        .then(() => {
          debug(`Created ${tableName} table`)
          app.emit(`table.${tableName}.created`)
        })
        .catch(e => console.error(`Error creating ${tableName} table`, e));
    }
    // At this point the table is not yet created!
    
  });

  return db;
};

there's nothing special in rootdir.class.js - just the standard stuff emitted by the generator:

const { Service } = require('feathers-knex');

exports.Rootdir = class Rootdir extends Service {
  constructor(options) {
    super({
      ...options,
      name: 'rootdir'
    });
  }
};

I need to initialize - that is to preload some data - the first time the table 'rootdir' is created. That must happen somewhere after service is created - but before the first request is served. The promise db.schema.hasTable(db.schema.createTable(...)) needs therefore to be fully resolved when it hits my 'initializing' code.

The only way I found around this was to:

  • emit an event 'table.${tableName}.created' when the table IS created
  • and then somewhere in my main code do:
app.on('table.rootdir.created' , async () => {
  await populateRootdirTable()
})

However, I feel like there SHOULD be a way to intercept the moment when:

  • the services are instantiated (app.hook['setup'])
  • AND the tables, if not existent, are created

What I have tried:

I hoped that the createModel could return a promise, which would then be resolved 'somewhere' within the Service. But no, the Service expects a 'initialized knex object' as 'Model' attribute. So, if I try to pass a promise instead of a reference to knex, the code fails.

I tried to put my initialization code in the app setup hook:

app.hooks({
  setup: [
    async function setupHook(context,next) {
      debug("setup hook called (before)");
      await next();
      const rootdirService = context.app.service('rootdir');
      const ret = await rootdirService.find({
        query: {
          $limit: 0
        }
      });
      console.log("ret:",ret);  
    }
  ],
  teardown: []
}
)

but again when the code reaches the hook, even though the service exists, the table is not yet created (Just for correctness: it DOES EXIST the second time you start the app, as by then the table would be created already; but it fails the first time)

Where should I put this 'initialization' code? How should I trigger it? Or, Am I thinking it wrong completely?


Solution

  • Since you are using SQL, table creations and data initialisation is done with migration files. In a normal Feathers 5 app you run the migrations with

    npm run migrate
    

    And example for how to create a migration is shown in the guide.

    Once you created the tables, in a migration you can also use the app object by importing it:

    import type { Knex } from 'knex'
    import { app } from '../src/app'
    
    export async function up(knex: Knex): Promise<void> {
      // create tables or make changes
      app.service('users').create(/* create admin user here */)
    }