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

sequelize-admin-panel

v1.0.3

Published

Admin panel for sequelize ORM

Downloads

3

Readme

Sequelize admin panel

TL;DR

Запустите

git clone https://github.com/eliseevmikhail/sequelize-admin-panel.git
cd sequelize-admin-panel
yarn install && cd demo && yarn install && yarn initdb && yarn start

и откройте localhost:3000

Основные возможности

Простая настройка

  • Просто подключите sequelizeAdmin как middleware по требуемому пути
  • В минимальной конфигурации достаточно передать экземпляр sequelize для получения рабочей админ-панели
  • Исходные модели не требуют модификации. Все дополнительные данные находятся в наследниках класса ModelAdmin

Настройка поведения полей

Возможно переопределение функций рендеринга полей моделей в списке записей и на экране редактирования записи

Поддержка типов и отношений

Поддерживается большинство примитивных типов Sequelize и все отношения (associations)

Пользователи и права

Встроенное управление пользователями и правами доступа на таблицы

Локализация

Все переводы, включая сообщения, названия моделей, полей и действий передаются в едином объекте, разбитом по локалям. Система определяет нужную локаль по заголовкам браузера. В случае отсутствия перевода используется встроенный английский для сообщений или название модели или поля для моделей и полей соответственно.

Быстрый старт

Создайте проект

mkdir sequelize-admin-demo; cd sequelize-admin-demo
yarn init
yarn add express sequelize sequelize-cli sequelize-admin-panel
yarn add mysql2 # или другой адаптер
node node_modules/.bin/sequelize init

отредактируйте config/config.json для подключения к вашей базе данных, рекомендуется сразу добавить "define": {"charset": "utf8", "collate": "utf8_general_ci"}, создайте БД

node node_modules/.bin/sequelize db:create

Создайте главный файл проекта index.js

const express = require('express')
const db = require('./models')
const { sequelizeAdmin } = require('sequelize-admin-panel')
const app = express()
app.use('/admin', sequelizeAdmin(express, db.sequelize))
app.listen(process.env.PORT || 3000, () => console.log('Server started'))

создайте файл cli.js

const db = require('./models')
require('sequelize-admin-panel').cli(db.sequelize)

обратите внимание, такая форма запуска работает потому что созданный по умолчанию файл ./models подключает все модели. Если вы не используете sequelize-cli, убедитесь что нужные модели импортированы (вызван Sequelize.define).

Создайте и синхронизируйте модели с помощью sequelize-cli

node node_modules/.bin/sequelize model:generate --name MyModel --attributes name:STRING,count:INTEGER
node node_modules/.bin/sequelize db:migrate
node ./cli init # инициализация таблицы пользователей

либо, если вы не хотите пользоваться sequelize migration, вручную создайте в каталоге models файлы моделей в соответствии с шаблоном:

module.exports = (sequelize, DataTypes) => {
  const MyModel = sequelize.define(
    'MyModel',
    {
      // <--- имя модели
      name: DataTypes.STRING,
      count: DataTypes.INTEGER
    },
    {}
  )
  MyModel.associate = function(models) {
    // associations can be defined here
    // like this: MyModel.belongsTo(models.OtherModel)
  }
  return MyModel
}

и синхронизируйте модели вызовом

node ./cli init --all # очистит все данные!

Недостатком первого способа является необходимость ручного описания изменений схем таблиц в файлах миграции, второго -- сброс содержимого базы данных при переинициализации. Подробнее о работе с sequelizejs по ссылке.

Примечание: попытка использовать Sequelize.sync({alter: true}) может приводить к дубликации constraints

Запустите сервер node . и откройте в браузере http://localhost:3000/admin. Готово!

Настройка представлений

Для настройки представления полей модели создайте наследника класса ModelAdmin, в функции init задайте требуемые значения, а затем передайте пару [MyModel,MyModelAdmin] в функцию sequelizeAdmin свойством models третьего аргумента.

MyModelAdmin.js:

const { ModelAdmin } = require('sequelize-admin-panel')

class MyModelAdmin extends ModelAdmin {
  repr(req, entry) {
    return entry.name
  }

  init() {
    super.init()
    this.list_fields = ['id', 'name', 'count', 'nonExistedField']
    this.list_links = ['id']
    this.search_fields = ['name', 'count']
    this.ordering = ['count', '-name']
    this.list_per_page = 20
    this.editor_fields = ['id', 'name', 'count']
    this.readonly_fields = ['id']
    this.icon = '<span class="oi oi-media-play"></span>'

    this.setFieldDescription('name', {
      view: (req, entry, fieldName) => {
        return 'I love ' + entry.name + '!'
      },
      html: false
    })

    this.setFieldDescription('count', {
      // можно и Promise
      view: (req, entry, fieldName) => {
        const model = req.SA.modelAdminInstance.model,
          Sequelize = req.SA.Sequelize
        // другие модели доступны через req.SA.modelAdminManager.getModelAdminByModelName('model_name').model
        return (
          model
            .find({
              // получаем максимальное
              attributes: [
                [Sequelize.fn('max', Sequelize.col('count')), 'max_count']
              ]
            })
            // простое экранирование
            .then(entry => parseInt(entry.get('max_count'), 10))
            .then(max => `<progress value="${entry.count}" max="${max}">`)
        )
      },
      html: true // осторожно -- вывод не экранируется
    })

    // можно создать сколько угодно псевдо-полей
    this.setFieldDescription('nonExistedField', {
      view: (req, entry, fieldName) => {
        return entry.name.toUpperCase() + ' is great!'
      }
    })
  }
}
module.exports = MyModelAdmin

index.js:

...
app.use('/admin', sequelizeAdmin(express, db.sequelize, {
  models: [ [db.MyModel, require('./MyModelAdmin')] ]
}))
...

В результате поле name будет признаваться в любви, count показывать progressbar относительно наибольшего в таблице значения, а третье поле, отсутсвующее в таблице, заниматься восхвалением.

Рассмотрим по пунктам:

  • Функция repr создаёт представление записи модели (строки таблицы). Оно используется в таблице результатов если не заданы поля в list_fields, а также для создания списков отношений. Обрабатывается как plain-text, можно возвращать Promise.
  • Свойства list_fields, list_links, search_fields, ordering задают соответственно какие поля будут видны в таблице результатов, какие из них являются ссылками, по каким осуществляется поиск и сортировка по умолчанию (в том числе в списках отношений). Надо отметить, что поиск и сортировка возможна только по примитивных типам, присутствующим в базе (т.е. не по associations и не по искусственным полям).
  • Если нужно вывести все поля кроме указанных, перечислите их в list_exclude. Если нужно вывести вообще все поля, укажите в list_exclude несуществующее поле. list_fields при этом должно быть пустым.
  • В list_fields можно указать несуществующее поле, а затем описать его представление.
  • В icon можно указать произвольный html, рекомендуется иконочный шрифт.

Далее, вызовом setFieldDescription мы переопределяем функцию рендеринга представления поля строки таблицы и указываем, следует ли её вывод интерпретировать как html. Не забывайте об экранировании.

Легко заметить, что каждый вызов view делает одну и ту же работу по вычислению максимального значения. Правильным решением будет вынести её в специальный коллбэк beforeListRender. Обратите внимание, возвращаемое значение передаётся как свойство объекта req:

class MyModelAdmin extends ModelAdmin {
...
  beforeListRender(req, count, entries) {
    const model = req.SA.modelAdminInstance.model,
      Sequelize = req.SA.Sequelize
    return model.find({
        // получаем максимальное
        attributes: [
          [Sequelize.fn('max', Sequelize.col('count')), 'max_count']
        ]
      })
      // простое экранирование и кэширование
      .then(entry => req.MAX = parseInt(entry.get('max_count'), 10))
  }
...
  init() {
    ...
    this.setFieldDescription('count', {
      view: (req, entry, fieldName) =>
        `<progress value="${entry.count}" max="${req.MAX}">`,
      html: true
    })
    ...
  }

Настройка виджетов

Функции setFieldDescription можно передать свойство widget, в котором указать функцию рендеринга виджета с сигнатурой (req, entry, fieldName, value, options) и возвращающей html-код виджета.

Например, напишем виждет для поля count в форме ползунка

    this.setFieldDescription('count', {
      ...
      widget: (req, entry, fieldName, value, options) =>
      `<input type="range" min="0" max="100" step="1"
        class="form-control"
        ${options.readOnly ? 'disabled' : ''}
        name=${fieldName}
        value=${value} />`
    })

Обратите внимание, нужно обязательно указать name=fieldName, желательно установить текущее значение value и флаг readonly. Класс form-control из bootstrap растягивает виджет по ширине и в общем случае не обязателен.

Если виджет сложнее поля ввода, следует создать скрытый input и помещать в него актуальное значение на клиентской стороне. Например, пусть поле point определено следующим образом:

point: {
  type: DataTypes.STRING,
  allowNull: false,
  defaultValue: '55.75027920193085,37.622483042183035',
}

тогда виджет с яндекс-картой можно описать так:

widget: (req, entry, fieldName, value) => {
  return `
      <!-- hidden form field -->
      <input type=hidden name="${fieldName}" value=${value} />
      <div style='width: 100%; height: 240px; border: solid black 1px' id="${fieldName}_mapid"></div>
      <script>
        function ${fieldName}_map() {
          var coord = '${value}'.split(',')
          var map = new ymaps.Map("${fieldName}_mapid", {
            center: coord, 
            zoom: 7
          });
          var placemark = new ymaps.Placemark(coord);
          map.events.add('click', function (e) {
            var coords = e.get('coords');
            placemark.geometry.setCoordinates(coords);
            // setup hidden form field
            document.getElementsByName("${fieldName}")[0].value=coords.join(',')
          });
          map.geoObjects.add(placemark);
        }
        ymaps.ready(${fieldName}_map);
      </script>`
}

Чтобы подключить библиотеку яндекс карт в методе init вызовите

this.addExtraResource(
  '<script src="https://api-maps.yandex.ru/2.1/?lang=ru_RU" type="text/javascript"></script>'
)

либо

// js в конце необходим парсеру
this.addExtraResource('https://api-maps.yandex.ru/2.1/?lang=ru_RU#js')

Теперь при любом нажатии на карту сериализованные координаты попадают в скрытый input, и передаются при сохранении на сервер с корректным именем поля. Отдельно стоит отметить, что при создании любых id лучше генерировать его с участием имени поля (${fieldName}_mapid) во избежание коллизий.

Настройка сеттеров

При сохранении записи модели каждое её поле сохраняется сеттером в соответствии с её типом. Переопределить функцию сохранения можно с помощью свойтства setter методов addFieldsDescriptions и setFieldDescription. В случае примитивного поля, сеттер по умолчанию выглядит так:

function defaultSetter(req, entry, fieldName, transaction) {
  entry[fieldName] = req.body[fieldName]
}

Так же может возвращать Promise. В сеттере можно поместить дополнительную проверку:

function setter(req, entry, fieldName, transaction) {
  if (req.body[fieldName] % 10 === 0) entry[fieldName] = req.body[fieldName]
  else throw req.SA.getSequelizeError('multiples of ten', fieldName)
}

Глобальные переопределения

Можно создать промежуточного наследника класса ModelAdmin, в котором переопределить функции рендеринга и сеттер для различных типов, в том числе несуществующих. А затем ссылаться на имена этих типов вместо определения функций в свойствах view, widget и setter методов addFieldsDescriptions и setFieldDescription:

class CustomTypes extends ModelAdmin {
  init() {
    super.init()
    this.overrideTypeView('X_FILEBLOB', (req, entry, fieldName) => {
      return entry[fieldName]
        ? req.SA.tr('File size:') + entry[fieldName].length
        : req.SA.tr('No file')
    })
    this.overrideTypeWidget(
      'X_FILEBLOB',
      (req, entry, fieldName, value, options) => {
        return `<input type="file" class="form-control" ${
          options.readOnly ? 'disabled' : ''
        } name="${fieldName}" />`
      }
    )
    this.overrideTypeSetter(
      'X_FILEBLOB',
      (req, entry, fieldName, transaction) => {
        return new Promise((resolve, reject) => {
          const file = req.files[fieldName]
          if (file.size > 1048576)
            reject(req.SA.getSequelizeError('file length', fieldName))
          fs.readFile(file.path, (err, buf) => {
            if (err) reject(err)
            else {
              entry[fieldName] = buf
              resolve()
            }
          })
        })
      }
    )
  }
}

class OverridedAdmin extends CustomTypes {
  init() {
    super.init()
    this.addFieldDescriptions({
      file: {
        view: 'X_FILEBLOB',
        widget: 'X_FILEBLOB',
        setter: 'X_FILEBLOB'
      }
    })
  }
}

Действия (actions)

Для массовой обработки результатов в таблице записей вы можете задать action.

Для этого передайте массив действий в свойство ModelAdmin.actions. Допустим, мы хотим иметь возможность обнулять поле count

this.actions = [
  {
    name: 'zerofy',
    renderer: (req, res, modelAdmin, ids, exit) => {
      let transaction
      return req.SA.sequelizeInstance
        .transaction()
        .then(_transaction => (transaction = _transaction))
        .then(() =>
          modelAdmin.model.update(
            { count: 0 },
            { where: { [modelAdmin.pkName]: ids }, transaction }
          )
        )
        .then(() => transaction.commit())
        .catch(() => transaction.rollback())
        .then(() => exit())
    }
  }
]

Теперь достаточно отметить чекбоксы соответствующих записей и выбрать пункт zerofy из выпадающего списка в заголовке колонки с чекбоксами. По умолчанию там присутствует только пункт delete.

Коллбэк renderer позволяет полностью управлять отображением и переходами между страницами, как это сделано в действии delete, но в данном случае явно избыточен. Вместо него можно в свойстве changer определить простой коллбэк, принимающий записи по одной:

this.actions = [
  {
    name: 'zerofy',
    changer: entry => (entry.count = 0)
  }
]

Не всегда оптимально, но определённо намного проще.

TODO: возврат ошибок и affected rows

Обработка параметров запроса и сквозные параметры

После редактирования записи модели желательно вернуться к просмотру списка записей в том же состоянии, включая пагинацию, поиск и т.д. Для упрощения этого, некоторые параметры должны автоматически передаваться при навигации. Реализовано это следующим образом. Есть предопределённый список параметров и функция, которая создаёт URL перехода включающего все эти параметры в сериализованном виде в свойстве params. Например, если мы находся на странице /admin/model/modelName/?search=тест, при переходе на вторую страницу результатов поиска, URL ссылки перехода будет содержать /admin/model/modelName?params={"search":"тест","page":1}.

Все URL переходов внутри админ-панели должны создаваться функцией req.SA.queryExtender(), принимающей следующие параметры:

  • массив частей пути относительно корня админ-панели ['model', 'modelName']
  • объект с новыми параметрами {page: 1}
  • массив параметров, которые не следует включать в запрос или false для исключния всех.

В результате вышеупомянутая ссылка пагинации выглядит так (используется pug):

a(href=req.SA.queryExtender(["model", req.SA.modelName], {page: data.page+1}, []) >>

Аналогично, при использовании форм, в форму следует включить поле сгенерированное req.SA.formExtender с теми же аргументами кроме первого

//- action формы создаётся без параметров запроса
form(action=req.SA.queryExtender(["model", req.SA.modelName], {}, false), enctype="multipart/form-data")
  //- поле формы
  input(type="text", name="search", value=req.SA.params.search)
  //- параметры кроме search и page вставлены в форму
  | !{req.SA.formExtender({}, ["search", "page"])}

На что следует обратить внимание:

  • action формы не должно содержать параметров
  • значение поля формы берётся из req.SA.params.search
  • при вызове req.SA.formExtender необходимо явно исключить свойство search, иначе после очистки поле поиска будет использовано старое значение
  • page очищается из тех соображений, что номер текущей страницы после изменения результатов вывода не имеет смысла

Подробнее о req.SA.params.search: в него попадают содержимое десериализованного поля params, параметры запроса, параметры форм в порядке перекрытия.

Список сквозных параметров: ['sort', 'page', 'search', 'subwindow', 'action', 'entryIds', 'backurl']

Валидация

Проверки корректности вводных данных лежат на sequelize, поэтому желательно описывать проверки при объявлении поля. Подробнее по ссылке

Собственные проверки можно поместить в сеттер.

Локализация

Запустите созданный ранее cli.js для получения объекта переводов

node cli dumptranslation [--empty] [--hints] > translations/[локаль].json

Отредактируйте локаль, переводы сообщений, имена моделей, полей и действий. Повторить по количеству локалей. Так же вы можете задать подсказки для моделей и полей, в первом случае через свойство hint, во втором через одноименное с именем поля свойство с добавленным суффиксом _hint

  "sequelize_admin_user": {
    "label": "",
    "plural": "",
    "hint": "подсказка модели",
    "fields": {
      "username": "",
      "username_hint": "подсказка поля",
  ...

Сгруппируйте переводы и передайте свойство translation в третий аргумент sequelizeAdmin:

app.use(
  '/admin',
  sequelizeAdmin(express, db.sequelize, {
    translation: Object.assign(
      {},
      require('./translations/ru'),
      require('./translations/de')
    )
  })
)

Автоматизация импорта содержимого папки translations на ваше усмотрение.

TODO: перевод ошибок

Управление пользователями и правами

Для создания суперпользователя выполните

node cli createsuperuser логин пароль

В режиме разработки (process.env.NODE_ENV !== 'production') это необязательно, при отстутствии суперпользователей осуществляется автоматический вход пользователя EMERGENCY и выводится предупреждение.

В список моделей добавлена sequelize_admin_users, в которой перечислены все пользователи, а также есть возможность управления правами доступа к моделям согласно CRUD.

Ограничения

  • подразумевается что primary key является числовым
  • работа c paranoid option не проверялась
  • для парсинга форм используется formidable, желательно использовать enctype="multipart/form-data". Настройки парсера можно передать в свойстве formidableOpts третьего параметра sequelizeAdmin
  • требуется NodeJS не ниже 6.0.0 версии, проверялось на 6.14.2LTS и 8.11.2LTS
  • сессии хранятся в памяти