Git Product home page Git Product logo

sheeted's Introduction

Sheeted

build status npm version

📎 Overview

Sheeted is a table UI web application framework.

It aims to make it extremely easy to develop table-based web applications, which are often used for organizations internal use or for some management use. With Sheeted, you will be released from boring coding not concerned with business rules. You can develop practical Table UI web applications 10x faster with Sheeted.

Features:

  • Auto generated REST API and UI
  • Flexibility to define business rules such as data structure, validations, and access policies
  • Authentication with SAML

📎 Getting Started

Sheeted provides CLI to create Sheeted app project. Run the command below:

$ npx @sheeted/cli project <your_project_name>

The command creates a directory named <your_project_name> and files all you need such as package.json. Then you can start to develop in the project.

$ cd <your_project_name>
$ cat README.md # You will find how to setup the project.

📎 Usage

A Sheeted web application consists of Sheets, which represent tables. A Sheet conststs of one type and some objects as below.

  • Entity type
  • Schema
  • View
  • AccessPolicies
  • Hook
  • Validator
  • Actions

Let's take a look one by one.

Entity type

Entity type is the data format of a row in Sheet. It's an interface in TypeScript. Every Entity must have "id" for unique identity. To ensure this, Entity type extends EntityBase.

Example:

import { EntityBase, IAMUserEntity } from '@sheeted/core'

import { Genre, Format } from '../../constants'

export interface BookEntity extends EntityBase {
  title: string
  like: number
  price: number
  genre: Genre
  formats: Format[]
  url?: string
  buyer: IAMUserEntity
  buyDate: number
  readFinishedAt: number
  readMinutes: number
  publicationYear: number
  comment?: string
}

Schema

Schema can define some properties of each field in Entitiy. It has the same fields as Entity's.

Example:

import { Types, IAM_USER_SHEET, Schema } from '@sheeted/core'

import { Genres, Formats } from '../../constants'

import { BookEntity } from './book.entity'

export const BookSchema: Schema<BookEntity> = {
  title: {
    type: Types.Text,
    unique: true,
  },
  like: {
    type: Types.Numeric,
    readonly: true,
  },
  price: {
    type: Types.Numeric,
  },
  genre: {
    type: Types.Enum,
    enumProperties: {
      values: Genres,
    },
  },
  formats: {
    type: Types.EnumList,
    enumProperties: {
      values: Formats,
    },
  },
  url: {
    type: Types.Text,
    optional: true,
  },
  buyer: {
    type: Types.Entity,
    readonly: true,
    entityProperties: {
      sheetName: IAM_USER_SHEET,
    },
  },
  buyDate: {
    type: Types.CalendarDate,
  },
  readFinishedAt: {
    type: Types.CalendarDatetime,
    optional: true,
  },
  readMinutes: {
    type: Types.Time,
  },
  publicationYear: {
    type: Types.CalendarYear,
  },
  comment: {
    type: Types.LongText,
    optional: true,
  },
}

View

View is about UI such as a column title.

Example:

import { View } from '@sheeted/core'
import { CALENDAR_DATETIME_FORMAT } from '@sheeted/core/build/interceptors'

import { BookEntity } from './book.entity'

export const BookView: View<BookEntity> = {
  title: 'Books',
  icon: 'menu_book',
  display: (entity) => entity.title,
  enableDetail: true,
  defaultSort: {
    field: 'title',
    order: 'asc',
  },
  columns: [
    { field: 'title', title: 'TITLE', style: { minWidth: '10em' } },
    { field: 'like', title: 'LIKE' },
    {
      field: 'price',
      title: 'PRICE',
      numericOptions: {
        formatWithIntl: {
          locales: 'ja-JP',
          options: { style: 'currency', currency: 'JPY' },
        },
      },
    },
    {
      field: 'genre',
      title: 'GENRE',
      enumLabels: { comic: 'COMIC', novel: 'NOVEL' },
    },
    {
      field: 'formats',
      title: 'FORMATS',
      enumLabels: { paper: 'PAPER', kindle: 'KINDLE' },
    },
    { field: 'url', title: 'URL', textOptions: { isLink: true } },
    { field: 'buyer', title: 'BUYER' },
    { field: 'buyDate', title: 'BUY DATE' },
    { field: 'readFinishedAt', title: 'FINISHED READING' },
    { field: 'readMinutes', title: 'READ TIME' },
    { field: 'publicationYear', title: 'YEAR OF PUBLICATION' },
    { field: 'comment', title: 'COMMENT', style: { minWidth: '15em' } },
    {
      field: 'updatedAt',
      title: 'LAST UPDATED',
      numericOptions: { formatAsDate: CALENDAR_DATETIME_FORMAT },
    },
  ],
}

AccessPolicies

AccessPolicies is a set of access policies based on roles. It's an array of AccessPolicy.

import { AccessPolicy, Context } from '@sheeted/core'

import { Roles, Role, ActionIds } from '../../constants'

import { BookEntity } from './book.entity'

export const BookAccessPolicies: AccessPolicy<BookEntity, Role>[] = [
  {
    action: 'read',
    role: Roles.DEFAULT_ROLE,
  },
  {
    action: 'create',
    role: Roles.DEFAULT_ROLE,
  },
  {
    action: 'update',
    role: Roles.DEFAULT_ROLE,
    column: {
      effect: 'deny',
      columns: ['genre'],
    },
    condition: (book: BookEntity, ctx?: Context<Role>): boolean => {
      return book.buyer && ctx?.user.id === book.buyer.id
    },
  },
  {
    action: 'update',
    role: Roles.EDITOR_ROLE,
  },
  {
    action: 'delete',
    role: Roles.DEFAULT_ROLE,
    condition: (book: BookEntity, ctx?: Context<Role>): boolean => {
      return book.buyer && ctx?.user.id === book.buyer.id
    },
  },
  {
    action: 'delete',
    role: Roles.EDITOR_ROLE,
  },
  {
    action: 'custom',
    role: Roles.DEFAULT_ROLE,
    customActionId: ActionIds.LIKE,
  },
]

Hook

Hook is a set of functions which will be executed after creating / updating / destroying entities.

Example:

import { Hook } from '@sheeted/core'
import { IAMUserRepository } from '@sheeted/mongoose'

import { BookEntity } from './book.entity'
import { BookRepository } from './book.repository'

export const BookHook: Hook<BookEntity> = {
  async onCreate(book, ctx, options) {
    const user = await IAMUserRepository.findById(ctx.user.id)
    if (!user) {
      throw new Error(`user not found for id "${ctx.user.id}"`)
    }
    await BookRepository.update(
      book.id,
      {
        buyer: user,
      },
      {
        transaction: options.transaction,
      },
    )
  },
}

Validator

Validator defines validations on creating / updating entities.

Example:

import { Validator, ValidationResult } from '@sheeted/core'

import { BookEntity } from './book.entity'

export const BookValidator: Validator<BookEntity> = (_ctx) => (
  input: Partial<BookEntity>,
  _current: BookEntity | null,
): ValidationResult<BookEntity> => {
  const result = new ValidationResult<BookEntity>()
  if (input.price) {
    if (!Number.isInteger(input.price)) {
      result.appendError({
        field: 'price',
        message: 'Must be integer',
      })
    }
    if (input.price < 0) {
      result.appendError({
        field: 'price',
        message: 'Must be greater than or equal to 0',
      })
    }
  }
  return result
}

Actions

Actions represents custom operations to entities. It's an array of Action.

Example:

import { Action } from '@sheeted/core'

import { ActionIds } from '../../constants'

import { BookEntity } from './book.entity'
import { BookRepository } from './book.repository'

export const BookActions: Action<BookEntity>[] = [
  {
    id: ActionIds.LIKE,
    title: 'Increment like count',
    icon: 'exposure_plus_1',
    perform: async (entity: BookEntity): Promise<void> => {
      await BookRepository.update(entity.id, {
        like: entity.like + 1,
      })
    },
  },
]

Sheet

Now we can define Sheet. It's the main object bundling above objects.

Example:

import { Sheet } from '@sheeted/core'

import { Role, SheetNames } from '../../constants'

import { BookEntity } from './book.entity'
import { BookSchema } from './book.schema'
import { BookValidator } from './book.validator'
import { BookView } from './book.view'
import { BookAccessPolicies } from './book.access-policies'
import { BookActions } from './book.actions'
import { BookHook } from './book.hook'

export const BookSheet: Sheet<BookEntity, Role> = {
  name: SheetNames.BOOK,
  Schema: BookSchema,
  Validator: BookValidator,
  View: BookView,
  AccessPolicies: BookAccessPolicies,
  Actions: BookActions,
  Hook: BookHook,
}

Creating app

After defining sheets, you can create application server with createApp(). This function just returns express app.

Function createApp() needs arguments as below.

  • AppTitle: title of application.
  • Sheets: sheets array.
  • Roles: role objects array.
  • DatabaseDriver: database driver. Currently only supported driver is mongo driver.
  • ApiUsers: array of an api user which has userId and accessToken. This is used for API access.

Example:

import { createApp } from '@sheeted/server'
import { MongoDriver } from '@sheeted/mongoose'

import { config } from '../util/config.util'
import { defaultUsers } from '../util/seeder.util'

import { RoleLabels } from './constants'
import { BookSheet } from './sheets/book/book.sheet'

const admin = defaultUsers[1]

export const app = createApp(
  {
    AppTitle: 'Book Management App',
    Sheets: [BookSheet],
    Roles: RoleLabels,
    DatabaseDriver: MongoDriver,
    ApiUsers: [
      {
        userId: admin.id,
        accessToken: 'f572d396fae9206628714fb2ce00f72e94f2258f',
      },
    ],
    options: {
      iamUserView: {
        title: 'User Management',
      },
    },
  },
  {
    ...config,
  },
)

More information

For more information about usage, please visit:

📎 TIPS

Can I add sheet sources easily?

You can create sheet source files via CLI.

$ npx @sheeted/cli sheet dir/to/sheet-name

Can I use a raw mongoose model of the sheet?

@sheeted/mongoose provides compileModel() function to access mongoose Models, or you can use the model from *.model.ts if you create a sheet via CLI.

📎 Generated REST API

You can use the generated REST API. The format of a response is JSON.

Common request headers

You need authorization header in every request which is defined in Application.ApiUsers.

Authorization: token <access token>

List all sheets

GET /api/sheets

Get a sheet

GET /api/sheets/:sheetName

List entities

GET /api/sheets/:sheetName/entities

Parameters

Name Type Description
page number a page number of list
limit number limit count of entities
search string search string
sort array of object sort objects

Get an entity

GET /api/sheets/:sheetName/entities/:entityId

Create an entity

POST /api/sheets/:sheetName/entities

Set JSON of an entity in the request body.

Update an entity

POST /api/sheets/:sheetName/entities/:entityId

Set JSON of changes in the request body.

Delete entities

POST /api/sheets/:sheetName/entities/delete

Set JSON of entity ids to be deleted as below.

{
  "ids": ["entityId1", "entityId2"]
}

📎 Development

Requirements:

  • Node.js >= 14
  • docker-compose
  • direnv

Install. This project uses yarn workspaces.

$ yarn install

Run docker containers.

$ docker-compose up -d

Run UI development server.

$ yarn w/ui start

Run an example app server.

$ node examples/build/account-management

Then, access to http://localhost:3000 on your browser and log in with demo/demo.

sheeted's People

Contributors

fujiharuka avatar

Stargazers

seikam avatar

Watchers

Takahiro Oohata avatar s-oku avatar rg_nakahata  avatar James Cloos avatar  avatar  avatar Tetsuya Ikenaga avatar  avatar

Forkers

kwryoh

sheeted's Issues

エンティティの詳細画面を追加

ユースケースとしては、申請フローの中である申請エンティティについて Slack などで「この申請についてなんだけど...」と会話する、など。そのためにエンティティにURLが必要。

追加行の編集位置と保存後の位置

現状、行追加ボタンを押すと新規行が一番下に現れ、保存すると一番上に来る(cf. #43 )。

ならば、保存する前から新規行が一番上に現れる方が自然に感じる。多分。(実際に見てみたら気が変わるかも。)

あるいは、保存した後も一番下のままにする(デフォルトのソート順を updatedAt に対し昇順にする)のもまた自然なように感じる。

つまり、上下どちらかに統一したい。

実行時に MongoError のワーニングが出る

例えば、npm examples/build/relation をした時のログ。

(node:9191) UnhandledPromiseRejectionWarning: MongoError: a collection 'relation.iamusers' already exists
    at MessageStream.messageHandler (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/sheeted/node_modules/mongodb/lib/cmap/connection.js:266:20)
    at MessageStream.emit (events.js:315:20)
    at processIncomingData (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/sheeted/node_modules/mongodb/lib/cmap/message_stream.js:144:12)
    at MessageStream._write (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/sheeted/node_modules/mongodb/lib/cmap/message_stream.js:42:5)
    at writeOrBuffer (_stream_writable.js:352:12)
    at MessageStream.Writable.write (_stream_writable.js:303:10)
    at Socket.ondata (_stream_readable.js:712:22)
    at Socket.emit (events.js:315:20)
    at addChunk (_stream_readable.js:302:12)
    at readableAddChunk (_stream_readable.js:278:9)
    at Socket.Readable.push (_stream_readable.js:217:10)
    at TCP.onStreamRead (internal/stream_base_commons.js:186:23)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:9191) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:9191) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

createdAt 等を画面に表示すること無しに、 createdAt 等を使ったソートを画面で切り替えたい

画面から { createdAt | updatedAt } で { 昇順 | 降順 } になるよう設定したい、という要求は普遍的に生じる気がする。

「普通は画面に表示するだろ(view に createdAt / updatedAt を設定するだろ)」という発想ならそれでも良いように思うが、「画面に表示するもしないも自由です」という意図でコンフィギュラブルにしているのなら、「表示はしたくないけどソートできなくなるのは不便」という声が挙がってもおかしくなさそうに感じた。

scatfolds が欲しい

新しいシートを作る時に、ファイル一式(XXX.access-policies.ts, XXX.entity.ts, XXX.hook.ts, etc.)いちいちコピペしてリネームして作るのが面倒。
XXX に相当する部分を指定してファイル一式を作れるコマンドが欲しい。

スキーマのパラメータ初期値改善提案

  • searchable は基本 true な気がするので、初期値 true の方が良さそう。
  • optional は true な項目の方が多い傾向にありそうな気がするので、初期値 false の方が良さそう。(もしくは optional ではなく required というパラメータを取るか。)

README 内、`createApp()` の引数の説明に抜けがある。

以下の README において

Function `createApp()` needs arguments as below.

- Sheets: sheets array.
- Roles: role objects array.
- DatabaseDriver: database driver. Currently only supported driver is mongo driver.
  • AppTitle
  • ApiUsers

の説明が不足している(その下のサンプルコードにはこれらが存在する)。

カレンダー年度に初期値が表示されているのに未入力扱いになる

発生手順

Schema にて

  year: {
    type: Types.CalendarYear,
    searchable: true,
  },

と設定して、新規行を作成する時、以下の画像の「年度」部分のように、初期値「2020」が入っている。

image

しかし、この「2020」に触らないまま登録しようとすると、見た目では「2020」が入っているにも関わらず "Required" と注意が出てしまう。

image

一度自分で「2020」を選び OK すれば登録することができる。

問題点

見た目上は値が入っているのに実際は未入力状態として扱われるのは、ユーザー視点では違和感がある。「数値が最初から入ってるから、特にいじらなくてもセーフなのかな。」と思ってしまい、"Required" と表示されて腹を立てながら直す、という流れが想像できる。

解決方針

  1. 未入力判定を表示通りに合わせる(=未入力な事態が起こり得なくなる)
    - 入力の手間をなるべく省いてあげる方向にデザインするならこっち
  2. 初期値を表示しない
    - "Required" による入力忘れ防止を徹底したいならこっち

入力補助の仕組みを他にも増やせそうであれば 2 の方が良いように思うが、そうでなければ 1 が良いように思う。

シート内データの編集をなるべくキーボードだけで完結させたい

以下のような仕様だと便利。

  • 新規行作成時に最左の入力欄に最初からカーソルがあっている。
  • Tab キーで遷移しながら入力できる。
    • 最右の入力欄に行った際に Tab キーを押すと、カーソルがどこか関係ないところに飛んでしまう。最左の入力欄に戻ってほしい。
    • カレンダーをキーで入力することができない。
  • Enter キーで行保存、Esc キーでキャンセル。

新規行登録時に "does not support detryable writes" が出て登録できない

image

MongoError: This MongoDB deployment does not support retryable writes. Please add retryWrites=false to your connection string.
    at getMMAPError (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/rg-expense-sheet/node_modules/mongodb/lib/core/topologies/shared.js:413:18)
    at handler (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/rg-expense-sheet/node_modules/mongodb/lib/core/sdam/topology.js:944:15)
    at /Users/ikngtty/Projects/src/github.com/realglobe-Inc/rg-expense-sheet/node_modules/mongodb/lib/cmap/connection_pool.js:354:13
    at handleOperationResult (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/rg-expense-sheet/node_modules/mongodb/lib/core/sdam/server.js:558:5)
    at commandResponseHandler (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/rg-expense-sheet/node_modules/mongodb/lib/core/wireprotocol/command.js:115:25)
    at MessageStream.messageHandler (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/rg-expense-sheet/node_modules/mongodb/lib/cmap/connection.js:266:11)
    at MessageStream.emit (events.js:315:20)
    at MessageStream.EventEmitter.emit (domain.js:485:12)
    at processIncomingData (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/rg-expense-sheet/node_modules/mongodb/lib/cmap/message_stream.js:144:12)
    at MessageStream._write (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/rg-expense-sheet/node_modules/mongodb/lib/cmap/message_stream.js:42:5)
    at writeOrBuffer (_stream_writable.js:352:12)
    at MessageStream.Writable.write (_stream_writable.js:303:10)
    at Socket.ondata (_stream_readable.js:712:22)
    at Socket.emit (events.js:315:20)
    at Socket.EventEmitter.emit (domain.js:485:12)
    at addChunk (_stream_readable.js:302:12) {
  originalError: MongoError: Transaction numbers are only allowed on a replica set member or mongos
      at MessageStream.messageHandler (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/rg-expense-sheet/node_modules/mongodb/lib/cmap/connection.js:266:20)
      at MessageStream.emit (events.js:315:20)
      at MessageStream.EventEmitter.emit (domain.js:485:12)
      at processIncomingData (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/rg-expense-sheet/node_modules/mongodb/lib/cmap/message_stream.js:144:12)
      at MessageStream._write (/Users/ikngtty/Projects/src/github.com/realglobe-Inc/rg-expense-sheet/node_modules/mongodb/lib/cmap/message_stream.js:42:5)
      at writeOrBuffer (_stream_writable.js:352:12)
      at MessageStream.Writable.write (_stream_writable.js:303:10)
      at Socket.ondata (_stream_readable.js:712:22)
      at Socket.emit (events.js:315:20)
      at Socket.EventEmitter.emit (domain.js:485:12)
      at addChunk (_stream_readable.js:302:12)
      at readableAddChunk (_stream_readable.js:278:9)
      at Socket.Readable.push (_stream_readable.js:217:10)
      at TCP.onStreamRead (internal/stream_base_commons.js:186:23) {
    ok: 0,
    code: 20,
    codeName: 'IllegalOperation'
  }
}
POST /api/sheets/Expense/entities 500 19.207 ms - 161

編集不可列を指定するのではなく、編集可能列を指定したい。

CreateAccessPolicy, UpdateAccessPolicyuneditableColumns? を指定できるようになっているが、editableColumns? のようなものも欲しい。

理由

例えば status という列のみを編集できる設定にしたい時、現状では status 以外の列を全て uneditableColumns? で指定しないといけない。作業が煩雑になるし、列追加時にここにも追加しなきゃいけないこととかいかにも忘れそう。

悩みどころ

とりあえず自分がパッと思いつくのは以下のような仕様だが、いまいち使い方が伝わりづらいというか、一言で言うとイケてない気がする。

  • editableColumns が指定されたら、指定された列は編集可能、指定されていない列は編集不可
  • uneditableColumns が指定されたら、指定された列は編集不可、指定されていない列は編集可能
  • 両方指定されたらエラー(コンパイル時に型エラーのようなものが出るのが望ましい)
  • 両方指定されなかったら全列編集可能

行追加中に行追加ボタンを押すと、保存してなかったデータが消える。

背景として、行追加ボタンを押してデータを入力したあと、保存操作を忘れがちになるということがある。
(むしろこの問題をどうにかする必要があるかも。 cf. #15

この状態のまま「もう一行追加しよ〜」と思って行追加ボタンを押してしまうと、保存し忘れたデータは黙って消えてしまう。

既存行編集の場合、行追加ボタンは disable になっているので、こうしたミスが起きることはない。
新規行追加の場合もこの仕様の方がいいように感じる。

(一応、「確認ダイアログを出す」という手もあるかもしれない。)

カスタムアクションをチェック前に表示したい

現状

  • チェックを入れている行がない場合

image

  • チェックを入れている行がある場合

image

右上のカスタムアクションアイコン2つとゴミ箱アイコンは、チェックを入れないと表示されないことが分かる。

問題点

最初から disable 状態のアイコンが表示されていれば、「チェックを入れればこれらのアクションができそうだな」と大体の人は想像つくと思う。
しかし、これらのアクションが最初は表示されないとなると、「あのアクションはどうやればいいのだろう…」としばらく考え込んだり、右クリックしてみたり、とかなってしまう気がする。

改善案

チェックの有無に関わらず、アイコンは全て表示されていた方が良いのではないか。
(ゴミ箱は今のままでもいいかもしれない。)

CLIを作る

$ sheeted generate <sheet name> すると Sheet の雛形ができるくらいにはしたい

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.