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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@msbci/form-server

v1.4.2

Published

Form server — REST API with Prisma, multi-tenant, multi-database

Readme

@msbci/form-server

npm version license

REST API server for form management — Prisma ORM, multi-tenant, PostgreSQL + SQLite. Compatible with Express, Fastify, and Next.js API Routes.

Installation

npm install @msbci/form-server @msbci/form-core @prisma/client
npm install -D prisma

Usage — Next.js API Routes

// app/api/forms/[...path]/route.ts
import { createFormRouter } from '@msbci/form-server'
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()
const router = createFormRouter({
  prisma,
  auth: async (req) => {
    const token = req.headers.authorization?.replace('Bearer ', '')
    const user = await verifyToken(token)
    return user ? { userId: user.id, tenantId: user.tenantId } : null
  },
})

export async function GET(req: Request) {
  const url = new URL(req.url)
  const path = url.pathname.replace('/api/forms', '')
  const result = await router.handle({
    method: 'GET',
    path,
    params: {},
    query: Object.fromEntries(url.searchParams),
    body: null,
    headers: Object.fromEntries(req.headers),
  })
  return Response.json(result.body, { status: result.status })
}

export async function POST(req: Request) {
  const url = new URL(req.url)
  const path = url.pathname.replace('/api/forms', '')
  const body = await req.json()
  const result = await router.handle({
    method: 'POST',
    path,
    params: {},
    query: Object.fromEntries(url.searchParams),
    body,
    headers: Object.fromEntries(req.headers),
  })
  return Response.json(result.body, { status: result.status })
}

Usage — Express

import express from 'express'
import { createFormRouter } from '@msbci/form-server'
import { PrismaClient } from '@prisma/client'

const app = express()
const router = createFormRouter({ prisma: new PrismaClient() })

app.use('/api/forms', express.json(), async (req, res) => {
  const result = await router.handle({
    method: req.method,
    path: req.path,
    params: {},
    query: req.query as Record<string, string>,
    body: req.body,
    headers: req.headers as Record<string, string>,
  })
  res.status(result.status).json(result.body)
})

Auth Injectable

The package includes no auth logic. Inject your own via the auth option:

import type { AuthMiddleware } from '@msbci/form-server'

const myAuth: AuthMiddleware = async (req) => {
  const token = req.headers.authorization?.replace('Bearer ', '')
  if (!token) return null // → 401 Unauthorized
  const user = await verifyJWT(token)
  return { userId: user.id, tenantId: user.orgId }
}

createFormRouter({ prisma, auth: myAuth })

Default: noAuth — allows all requests (for development).

Multi-Tenant

tenantId is an optional field on FormDefinition and FormSubmission:

  • Pass tenantId when creating forms and submissions
  • Filter by tenantId in list queries
  • Auth middleware can set tenantId from the authenticated user

Database

| Provider | Use case | URL format | |----------|----------|------------| | PostgreSQL | Production | postgresql://user:pass@host:5432/db | | SQLite | Development / Testing | file:./dev.db |

Copy the Prisma schema from the package and adjust the provider:

datasource db {
  provider = "postgresql"  // or "sqlite"
  url      = env("DATABASE_URL")
}

Routes (25)

| Method | Path | Description | |--------|------|-------------| | GET | /form-types | List all form types | | GET | /form-types/:id | Get form type | | POST | /form-types | Create form type | | PUT | /form-types/:id | Update form type | | DELETE | /form-types/:id | Delete form type | | GET | /forms | List forms (paginated, searchable) | | GET | /forms/:id | Get form with full structure | | POST | /forms | Create form | | PUT | /forms/:id | Update form | | DELETE | /forms/:id | Delete form (cascade) | | GET | /forms/:id/export | Export form as JSON | | POST | /forms/import | Import form from JSON | | POST | /forms/:formId/pages | Create page | | PUT | /pages/:id | Update page | | DELETE | /pages/:id | Delete page | | POST | /pages/:pageId/rosters | Create roster | | PUT | /rosters/:id | Update roster | | DELETE | /rosters/:id | Delete roster | | POST | /pages/:pageId/variables | Create variable on page | | POST | /rosters/:rosterId/variables | Create variable on roster | | PUT | /variables/:id | Update variable | | DELETE | /variables/:id | Delete variable | | GET | /forms/:formId/submissions | List submissions (paginated) | | GET | /submissions/:id | Get submission | | POST | /submissions | Create submission | | PUT | /submissions/:id | Update submission status |

All inputs validated with Zod. Errors returned as { status, message, data, errors }.

v1.4.2

  • Import sans collision de codePOST /forms/import derivait un code identique a la source ; dans un tenant donne, la contrainte @@unique([code, version, tenantId]) etait violee (erreur 500 a la duplication d'un formulaire). L'import suffixe desormais le code (-COPY, -COPY-2, ...) lorsqu'il existe deja pour ce tenant. Sans tenant (NULL), le code est preserve (les NULL ne collisionnent pas en SQL) : comportement inchange. Test d'integration ajoute.

v1.4.1

  • Documentation : retrait d'une référence à un hôte spécifique dans le changelog au profit d'une formulation générique (l'application hôte). Aucun changement de code ni d'API.

v1.4.0

  • GET /forms/:id renvoie la forme éditable — la route retournait l'objet Prisma brut (pivot scopes, JSON sérialisé) au lieu du type domaine IFormDefinition. Conséquence : form.scopeIds était absent et les scopes liés disparaissaient au rechargement dans l'éditeur. La route renvoie désormais getEditableFormDefinition (= dbToFormDefinition sans résolution de templates) : scopeIds présents, JSON parsé, et templateId/templateOverrides conservés pour permettre l'édition (contrairement à /forms/:id/resolved qui fusionne les templates). Test d'intégration ajouté.
  • Aucun changement de schéma. Bump mineur car la forme de réponse de GET /forms/:id change (alignée sur le type documenté).

v1.3.3 – v1.3.5

  • Persistance profonde des pagesupdateFormDefinition (PUT du formulaire complet par l'éditeur visuel) fait un deep-upsert des pages, variables et rosters via persistPages, partagé avec importForm/createFormDefinition. Les pages ne se perdaient plus entre deux enregistrements.
  • Champs de mise en pageFormVariable.startWithNewLine Boolean? et colSpan Int? (colonnes nullables, additives) persistés et relus (extractVariableData/dbToVariable).
  • Écritures multi-tenant sûres — le tenantId des formulaires (et, côté application hôte, des scopes/templates) est dérivé du contexte de la requête plutôt que de la confiance au client.
  • Tolérance aux champs optionnels à null côté hôte et republication corrigeant le protocole workspace:* dans le tarball publié (installation autonome via pnpm publish).

v1.3.2

  • DataSourceConfig CRUD — nouvelle table data_source_configs (@@unique([code, scopeId]), onDelete: Cascade depuis Scope). 6 nouveaux endpoints REST :
    • GET / POST /scopes/:scopeId/datasources
    • GET / PUT / DELETE /datasources/:id
    • GET /forms/:formId/datasourcesendpoint agrégé : renvoie { items: IDataSourceConfig[], scopeHeaders: IScopeHeaders[] } (configs + headers décryptés des scopes du form, tous en un seul appel).
  • Chiffrement AES-256-GCM des headers admin via src/utils/encryption.ts. Format base64(iv):base64(authTag):base64(ciphertext), IV 12 bytes (recommandé GCM), auth tag 16 bytes.
    • MOSOBI_ENCRYPTION_KEY (32 bytes en base64 ou 64 chars hex) est requis pour écrire des headers : requireEncryption() lève 503 Service Unavailable sinon.
    • Lecture tolérante : si la clé manque, decrypt() renvoie {} avec un warning console — la lecture n'est jamais bloquée (la form reste rendable).
  • Headers de Scope : Scope.headers String? (chiffré JSON). Surfacé décrypté dans IScope.headers sur GET. Mergés avec IDataSourceConfig.headersOverride côté renderer (config gagne).
  • Suppression d'un scope refusée en 409 si au moins une DataSourceConfig le référence (en plus des forms et templates existants).
  • Nouveau helper ApiError.serviceUnavailable(message) pour les 503.
  • Migration Postgres 20260524000000_datasource_config fournie pour apps/demo.
  • 9 nouveaux tests d'intégration (54 total) : CRUD complet, dédup code, agrégation par formId, refus 503 sans clé, chiffrement effectif en base (le bearer n'apparaît jamais en clair), suppression scope bloquée par data source.
  • Tests serveur passent en fileParallelism: false pour éviter les courses sur la même test.db.

v1.3.1

  • Multi-scopes par formulaire — la colonne FormDefinition.scopeId est remplacée par une table pivot FormDefinitionScope (formId, scopeId, order) avec onDelete: Cascade de chaque côté. L'ordre dans le pivot porte la priorité des scopes.
  • POST /forms et PUT /forms/:id acceptent désormais scopeIds: string[] (validé par Zod). Le payload scopeId n'est plus reconnu.
  • GET /forms?scopeId=X filtre via scopes: { some: { scopeId } } (transparent pour le client).
  • GET /forms/:id et GET /forms/:id/resolved retournent scopeIds triés par order asc.
  • dbToFormDefinition extrait scopeIds depuis la relation scopes ordonnée.
  • Migration Postgres 20260523000000_scope_pivot fournie pour apps/demo (drop column → create pivot + indexes + FKs). Côté serveur (SQLite test) : prisma db push synchronise le schéma au démarrage des tests.
  • Suppression d'un scope refusée en 409 si au moins une ligne pivot le référence (utilise formDefinitionScope.count).
  • 2 nouveaux tests d'intégration (45 total) : préservation de l'ordre des scopes sur GET /forms/:id/resolved, remplacement complet de la liste sur PUT /forms/:id.

v1.3.0

  • Scope : CRUD complet (/scopes) + multi-tenant via tenantId @default("default")
  • VariableTemplate : CRUD complet (/scopes/:scopeId/templates, /templates/:id) — code et name immutables (enforced Zod .strict() + service)
  • GET /forms/:id/resolved — formulaire résolu via Option B (templates appliqués in-memory)
  • 10 nouveaux endpoints REST (/scopes, /scopes/:id, /scopes/:scopeId/templates, /templates/:id, /forms/:id/resolved)
  • Suppression scope refusée en 409 si forms OU templates rattachés
  • Tenant default créé automatiquement au boot via initializeDefaults (idempotent, WeakSet<PrismaClient>)
  • FormDefinition.scopeId + FormVariable.templateId / templateOverrides (champs Prisma)
  • 10 nouveaux tests d'intégration (43 total)

v1.2.0

  • LocalizedString sérialisé en JSON côté persistence
  • Compatible avec les schémas multilingues @msbci/form-core v1.2.0
  • No breaking changes

What's new in v1.1.0

  • Version aligned with @msbci/form-core v1.1.0. The server transparently persists the extended IFieldResponseMetadata (8 new optional fields including displayValue, variableLabel, page / roster context) when host applications forward responses from FormRenderer — no API or schema change required.
  • No breaking change. Drop-in upgrade from v1.0.x.

License

Copyright (c) 2026 MOSOBI — All rights reserved. Commercial license required. Contact: [email protected]