...
In a LiteFarm deployment, the database runs as litefarm-db
, a Docker container built from the official Postgres image.
The API code
An Application Programming Interface (API) is an interface that a software product provides for use by other software, rather than by human users.
...
Auto-Increment ID Conventions as of August 2024
In our migration strategy:
Manually Set IDs: Used for the
permissions
table to ensure consistent IDs across different environments (local, beta, production).Previously, hard-coding IDs led to "UniqueViolationError" issues when inserting new rows.
Jira Legacy server System Jira serverId 815f41e5-e5fb-3402-8587-82eccc3ffab0 key LF-3651
Auto-Increment IDs: Applied by default unless there's a specific reason to manually set IDs. This helps avoid conflicts and simplifies record management.
The API code
An Application Programming Interface (API) is an interface that a software product provides for use by other software, rather than by human users.
The code for the LiteFarm API is in the packages/api/src
folder. In a LiteFarm deployment, this Node.js server runs in a Docker container named litefarm-api
. The container is built to put the API code in a standard Node image, and also uses a standard Nginx image to create a reverse proxy server.
...
Code Block | ||
---|---|---|
| ||
/** * Models data persistence for users' notifications. */ class NotificationUser extends baseModel { /** * Tracks open subscription channels for server-sent events. To support multiple sessions by the same user, * keys are user IDs; values are Maps with timestamp keys and HTTP response object values. * @member {Map} * @static */ static subscriptions = new Map(); /** * Identifies the database table for this Model. * @static * @returns {string} Names of the database table. */ static get tableName() { return 'notification_user'; } /** * Identifies the primary key fields for this Model. * @static * @returns {string[]} Names of the primary key fields. */ static get idColumn() { return ['notification_id', 'user_id']; } /** * Supports validating instances of this Model class. * @static * @returns {Object} A description of valid instances. */ static get jsonSchema() { return { type: 'object', required: ['user_id'], properties: { notification_id: { type: 'string' }, user_id: { type: 'string' }, alert: { type: 'boolean' }, status: { type: 'string', /** * @name userNotificationStatusType * @desc Enumerated type for user notification status. * @enum * */ enum: ['Unread', 'Read', 'Archived'], }, ...this.baseProperties, }, additionalProperties: false, }; } /** * Defines this Model's associations with other Models. * @static * @returns {Object} A description of Model associations. */ static get relationMappings() { return { notification: { relation: Model.BelongsToOneRelation, modelClass: Notification, join: { from: 'notification_user.notification_id', to: 'notification.notification_id', }, }, }; } // other custom methods not shown here. } |
...
tableName()
tells Objection the database table name for this modelidColumn()
indicates the table columns needed to uniquely identify a table rowjsonSchema()
supports validating the data contents of class instances before database modifications are made. Objection uses the embedded information to determine the appropriate data types, which values are required to be present, etc.relationMappings()
describes how this model is related to others. Each instance of this class belongs to one instance of theNotification
model class.templateMappingSchema()
(not shown) this is a custom function created by LiteFarm to define how one would treat properties and relations of the models graph in a duplication context (example: repeat management plans)
In jsonSchema()
, the spread syntax expression ...this.baseProperties
is used to include the base model's "base properties" in validating instances of class NotificationUser
. These properties are "bookkeeping" columns found in most LiteFarm database tables. They record who created the row, and when; who last updated the row, and when; and whether the row is marked as "deleted". (In most cases, LiteFarm performs "soft deletes"; rather than actually deleting a database row, the row is marked as deleted, and excluded from further use.)
...
Like a route handler, middleware functions receive Express's req
and res
objects as arguments. They also receive an argument named next
, which represents the next function listed for the endpoint. A middleware function can choose to call that next function or not; this means it can permit control to continue through the list of functions, or interrupt the flow.
For example, the middleware function above queries the Objection userFarmModel
class to determine if the user making this request is an authorized user for the farm identified in the request. If so, the middleware calls next()
which activates the route handler that we have already discussed. But if the user is not authorized for the farm, the middleware does not use next
to activate the route handler. Instead, it handles the response itself by sending an HTTP status code 403 ("Forbidden") and a description of the problem.
LiteFarm generally follows the REST conventions for endpoints and status codes used in responses.
Backend tests
The packages/api/tests
folder contains tests of the API built with Jest. From a terminal in the packages/api
folder, you can use the command npm test
to run all these tests. Or, npx jest --runInBand tests/notificationUser.test.js
will run a single file of tests-- in this case, the tests for the code we have explored. Here is an excerpt.
Code Block | ||
---|---|---|
| ||
const mocks = require('./mock.factories');
describe('Notification tests', () => {
function getRequest(url, { user_id = user.user_id, farm_id = farm.farm_id }, callback) {
chai.request(server).get(url).set('user_id', user_id).set('farm_id', farm_id).end(callback);
}
let user;
let farm;
let userFarm;
beforeEach(async () => {
[user] = await mocks.usersFactory();
[farm] = await mocks.farmFactory();
[userFarm] = await mocks.userFarmFactory({ promisedUser: [user], promisedFarm: [farm] });
const middleware = require('../src/middleware/acl/checkJwt');
middleware.mockImplementation((req, res, next) => {
req.user = {};
req.user.user_id = req.get('user_id');
next();
});
});
describe('GET user notifications', () => {
test('Users should get their notifications scoped for their current farm', async (done) => {
const [notification] = await mocks.notification_userFactory({
promisedUserFarm: [userFarm],
});
getRequest('/notification_user', {}, (err, res) => {
expect(err).toBe(null);
expect(res.status).toBe(200);
expect(res.body.length).toBe(1);
expect(res.body[0].user_id).toBe(user.user_id);
expect(res.body[0].notification_id).toBe(notification.notification_id);
done();
});
}); |
beforeEach
configures the Jest framework to run the setup needed before each individual test. describe
is used to organize groups of tests. test
defines an individual test with an English description and a function that receives done
. The argument is a callback function that the test must call when it has finished.
The test shown relies on database contents created by beforeEach
. The userFarm
object contains details of these database contents, and the test passes it to notification_userFactory
to create additional database content specific to this test. This establishes known database contents as a basis for testing data retrieval by the API route "under test". (The mocks
collection defines "factory" functions for most LiteFarm database record types. These support testing by creating database records as needed.)
The test then uses a helper function to make a GET
request to the URL for retrieving the user's notifications for their current farm. This executes the code under test, which we expect to retrieve known data values. When the request completes, Jest runs the anonymous function that receives two arguments representing any err
or, and the API res
ponse.
expect
calls are the actual testing; they describe the expected results. Conditional logic is embedded within expect
so that any unsatisfied expectations will cause the test to fail. If all expectations are met and the code calls done()
, then the test passes.
The output below includes the English provided to the describe
and test
calls above. The second green check mark indicates that the test shown in the excerpt has passed.
...
For example, the middleware function above queries the Objection userFarmModel
class to determine if the user making this request is an authorized user for the farm identified in the request. If so, the middleware calls next()
which activates the route handler that we have already discussed. But if the user is not authorized for the farm, the middleware does not use next
to activate the route handler. Instead, it handles the response itself by sending an HTTP status code 403 ("Forbidden") and a description of the problem.
LiteFarm generally follows the REST conventions for endpoints and status codes used in responses.
Backend tests
The packages/api/tests
folder contains tests of the API built with Jest. You can read more about this here.