nodejs-store

Admin CRUD backend

The problem

You are building an internal admin tool: list, view, edit and archive records for a handful of models. The work is repetitive — every model needs list pagination with a total count, a detail view, guarded writes, a delete that can be undone, and some derived columns (counts, totals) that the UI shows but that are not stored in the table. On top of that, different staff roles should see and do different things, and an editor should only touch their own records.

Why nodejs-store

Walkthrough

const { store, PermissionError } = require('nodejs-store');

store.register({
  name: 'Course',
  collection: 'courses',
  idPrefix: 'c',
  timestamps: true,
  fields: {
    title: { type: 'string', default: '' },
    status: { type: 'string', default: 'draft' },
    price: { type: 'float', default: 0 },
    createdBy: { type: 'string' },
  },
  relations: {
    lessons: { model: 'Lesson', type: 'many', localField: '_id', foreignField: 'courseId' },
  },
  computes: {
    lessonCount: { type: 'int', agg: { $count: 'lessons' } },
    revenue: { type: 'float', depends: ['price'], fn: (d) => d.price * 1.2 },
  },
  indexes: [{ keys: { status: 1, createdAt: -1 } }],
  read: ['admin', 'editor', 'creator'],
  write: ['admin', 'editor', 'creator'],
});

// Once per request (middleware layer).
store.setContext({ userId: req.user.id, roles: req.user.roles });

// List — page/pageSize are read from the params object; pageSize is capped at 5000.
const page = await store.queryWithCount(
  'Course($condition:@c0,$sort:@s0){ _id, title, status, lessonCount, revenue }',
  { c0: { status: 'published' }, s0: { createdAt: -1 }, page: 0, pageSize: 50 },
);
// page = { items, total, hasMore, page, pageSize }

// Update — plain fields become a $set; '$'-prefixed keys pass through as operators.
await store.update('Course', { _id: id }, { status: 'published' });
await store.update('Course', { _id: id }, { $inc: { enrolledCount: 1 } });

// Soft delete — archives to courses_deleted first (upsert-by-_id), then deletes.
const r = await store.remove('Course', { _id: id });
// r = { deletedCount, archivedCount }

// Empty conditions never reach the database.
await store.remove('Course', {}); // rejected instead of deleting the whole table

Pitfalls

See also