@msbci/form-server 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md CHANGED
@@ -1,7 +1,7 @@
1
- Copyright (c) 2026 AFINOV SARL — All rights reserved.
2
-
3
- This software is proprietary and confidential.
4
- Usage requires a valid commercial license from AFINOV SARL.
5
- Unauthorized use, reproduction or distribution is strictly prohibited.
6
-
7
- Contact: info@afinov.net
1
+ Copyright (c) 2026 AFINOV SARL — All rights reserved.
2
+
3
+ This software is proprietary and confidential.
4
+ Usage requires a valid commercial license from AFINOV SARL.
5
+ Unauthorized use, reproduction or distribution is strictly prohibited.
6
+
7
+ Contact: info@afinov.net
package/README.md CHANGED
@@ -1,213 +1,213 @@
1
- # @msbci/form-server
2
-
3
- [![npm version](https://img.shields.io/npm/v/@msbci/form-server)](https://www.npmjs.com/package/@msbci/form-server)
4
- [![license](https://img.shields.io/badge/license-Commercial-blue)](./LICENSE.md)
5
-
6
- REST API server for form management — **Prisma ORM, multi-tenant, PostgreSQL + SQLite**. Compatible with Express, Fastify, and Next.js API Routes.
7
-
8
- ## Installation
9
-
10
- ```bash
11
- npm install @msbci/form-server @msbci/form-core @prisma/client
12
- npm install -D prisma
13
- ```
14
-
15
- ## Usage — Next.js API Routes
16
-
17
- ```typescript
18
- // app/api/forms/[...path]/route.ts
19
- import { createFormRouter } from '@msbci/form-server'
20
- import { PrismaClient } from '@prisma/client'
21
-
22
- const prisma = new PrismaClient()
23
- const router = createFormRouter({
24
- prisma,
25
- auth: async (req) => {
26
- const token = req.headers.authorization?.replace('Bearer ', '')
27
- const user = await verifyToken(token)
28
- return user ? { userId: user.id, tenantId: user.tenantId } : null
29
- },
30
- })
31
-
32
- export async function GET(req: Request) {
33
- const url = new URL(req.url)
34
- const path = url.pathname.replace('/api/forms', '')
35
- const result = await router.handle({
36
- method: 'GET',
37
- path,
38
- params: {},
39
- query: Object.fromEntries(url.searchParams),
40
- body: null,
41
- headers: Object.fromEntries(req.headers),
42
- })
43
- return Response.json(result.body, { status: result.status })
44
- }
45
-
46
- export async function POST(req: Request) {
47
- const url = new URL(req.url)
48
- const path = url.pathname.replace('/api/forms', '')
49
- const body = await req.json()
50
- const result = await router.handle({
51
- method: 'POST',
52
- path,
53
- params: {},
54
- query: Object.fromEntries(url.searchParams),
55
- body,
56
- headers: Object.fromEntries(req.headers),
57
- })
58
- return Response.json(result.body, { status: result.status })
59
- }
60
- ```
61
-
62
- ## Usage — Express
63
-
64
- ```typescript
65
- import express from 'express'
66
- import { createFormRouter } from '@msbci/form-server'
67
- import { PrismaClient } from '@prisma/client'
68
-
69
- const app = express()
70
- const router = createFormRouter({ prisma: new PrismaClient() })
71
-
72
- app.use('/api/forms', express.json(), async (req, res) => {
73
- const result = await router.handle({
74
- method: req.method,
75
- path: req.path,
76
- params: {},
77
- query: req.query as Record<string, string>,
78
- body: req.body,
79
- headers: req.headers as Record<string, string>,
80
- })
81
- res.status(result.status).json(result.body)
82
- })
83
- ```
84
-
85
- ## Auth Injectable
86
-
87
- The package includes **no auth logic**. Inject your own via the `auth` option:
88
-
89
- ```typescript
90
- import type { AuthMiddleware } from '@msbci/form-server'
91
-
92
- const myAuth: AuthMiddleware = async (req) => {
93
- const token = req.headers.authorization?.replace('Bearer ', '')
94
- if (!token) return null // → 401 Unauthorized
95
- const user = await verifyJWT(token)
96
- return { userId: user.id, tenantId: user.orgId }
97
- }
98
-
99
- createFormRouter({ prisma, auth: myAuth })
100
- ```
101
-
102
- Default: `noAuth` — allows all requests (for development).
103
-
104
- ## Multi-Tenant
105
-
106
- `tenantId` is an optional field on `FormDefinition` and `FormSubmission`:
107
-
108
- - Pass `tenantId` when creating forms and submissions
109
- - Filter by `tenantId` in list queries
110
- - Auth middleware can set `tenantId` from the authenticated user
111
-
112
- ## Database
113
-
114
- | Provider | Use case | URL format |
115
- |----------|----------|------------|
116
- | **PostgreSQL** | Production | `postgresql://user:pass@host:5432/db` |
117
- | **SQLite** | Development / Testing | `file:./dev.db` |
118
-
119
- Copy the Prisma schema from the package and adjust the `provider`:
120
-
121
- ```prisma
122
- datasource db {
123
- provider = "postgresql" // or "sqlite"
124
- url = env("DATABASE_URL")
125
- }
126
- ```
127
-
128
- ## Routes (25)
129
-
130
- | Method | Path | Description |
131
- |--------|------|-------------|
132
- | `GET` | `/form-types` | List all form types |
133
- | `GET` | `/form-types/:id` | Get form type |
134
- | `POST` | `/form-types` | Create form type |
135
- | `PUT` | `/form-types/:id` | Update form type |
136
- | `DELETE` | `/form-types/:id` | Delete form type |
137
- | `GET` | `/forms` | List forms (paginated, searchable) |
138
- | `GET` | `/forms/:id` | Get form with full structure |
139
- | `POST` | `/forms` | Create form |
140
- | `PUT` | `/forms/:id` | Update form |
141
- | `DELETE` | `/forms/:id` | Delete form (cascade) |
142
- | `GET` | `/forms/:id/export` | Export form as JSON |
143
- | `POST` | `/forms/import` | Import form from JSON |
144
- | `POST` | `/forms/:formId/pages` | Create page |
145
- | `PUT` | `/pages/:id` | Update page |
146
- | `DELETE` | `/pages/:id` | Delete page |
147
- | `POST` | `/pages/:pageId/rosters` | Create roster |
148
- | `PUT` | `/rosters/:id` | Update roster |
149
- | `DELETE` | `/rosters/:id` | Delete roster |
150
- | `POST` | `/pages/:pageId/variables` | Create variable on page |
151
- | `POST` | `/rosters/:rosterId/variables` | Create variable on roster |
152
- | `PUT` | `/variables/:id` | Update variable |
153
- | `DELETE` | `/variables/:id` | Delete variable |
154
- | `GET` | `/forms/:formId/submissions` | List submissions (paginated) |
155
- | `GET` | `/submissions/:id` | Get submission |
156
- | `POST` | `/submissions` | Create submission |
157
- | `PUT` | `/submissions/:id` | Update submission status |
158
-
159
- All inputs validated with Zod. Errors returned as `{ status, message, data, errors }`.
160
-
161
- ## v1.3.2
162
-
163
- - **DataSourceConfig CRUD** — nouvelle table `data_source_configs` (`@@unique([code, scopeId])`, `onDelete: Cascade` depuis Scope). 6 nouveaux endpoints REST :
164
- - `GET / POST /scopes/:scopeId/datasources`
165
- - `GET / PUT / DELETE /datasources/:id`
166
- - `GET /forms/:formId/datasources` — **endpoint agrégé** : renvoie `{ items: IDataSourceConfig[], scopeHeaders: IScopeHeaders[] }` (configs + headers décryptés des scopes du form, tous en un seul appel).
167
- - **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.
168
- - `MOSOBI_ENCRYPTION_KEY` (32 bytes en base64 ou 64 chars hex) est requis pour **écrire** des headers : `requireEncryption()` lève `503 Service Unavailable` sinon.
169
- - **Lecture tolérante** : si la clé manque, `decrypt()` renvoie `{}` avec un warning console — la lecture n'est jamais bloquée (la form reste rendable).
170
- - **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).
171
- - Suppression d'un scope refusée en **409** si au moins une `DataSourceConfig` le référence (en plus des forms et templates existants).
172
- - Nouveau helper `ApiError.serviceUnavailable(message)` pour les 503.
173
- - Migration Postgres `20260524000000_datasource_config` fournie pour `apps/demo`.
174
- - 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.
175
- - Tests serveur passent en `fileParallelism: false` pour éviter les courses sur la même `test.db`.
176
-
177
- ## v1.3.1
178
-
179
- - **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.
180
- - `POST /forms` et `PUT /forms/:id` acceptent désormais `scopeIds: string[]` (validé par Zod). Le payload `scopeId` n'est plus reconnu.
181
- - `GET /forms?scopeId=X` filtre via `scopes: { some: { scopeId } }` (transparent pour le client).
182
- - `GET /forms/:id` et `GET /forms/:id/resolved` retournent `scopeIds` triés par `order` asc.
183
- - `dbToFormDefinition` extrait `scopeIds` depuis la relation `scopes` ordonnée.
184
- - 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.
185
- - Suppression d'un scope refusée en **409** si au moins une ligne pivot le référence (utilise `formDefinitionScope.count`).
186
- - 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`.
187
-
188
- ## v1.3.0
189
-
190
- - **Scope** : CRUD complet (`/scopes`) + multi-tenant via `tenantId @default("default")`
191
- - **VariableTemplate** : CRUD complet (`/scopes/:scopeId/templates`, `/templates/:id`) — `code` et `name` immutables (enforced Zod `.strict()` + service)
192
- - `GET /forms/:id/resolved` — formulaire résolu via Option B (templates appliqués in-memory)
193
- - 10 nouveaux endpoints REST (`/scopes`, `/scopes/:id`, `/scopes/:scopeId/templates`, `/templates/:id`, `/forms/:id/resolved`)
194
- - Suppression scope refusée en **409** si forms OU templates rattachés
195
- - **Tenant `default` créé automatiquement** au boot via `initializeDefaults` (idempotent, `WeakSet<PrismaClient>`)
196
- - `FormDefinition.scopeId` + `FormVariable.templateId` / `templateOverrides` (champs Prisma)
197
- - 10 nouveaux tests d'intégration (43 total)
198
-
199
- ## v1.2.0
200
-
201
- - `LocalizedString` sérialisé en JSON côté persistence
202
- - Compatible avec les schémas multilingues `@msbci/form-core` v1.2.0
203
- - No breaking changes
204
-
205
- ## What's new in v1.1.0
206
-
207
- - 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.
208
- - No breaking change. Drop-in upgrade from v1.0.x.
209
-
210
- ## License
211
-
212
- Copyright (c) 2026 MOSOBI — All rights reserved.
213
- Commercial license required. Contact: dev@mosobi.com
1
+ # @msbci/form-server
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@msbci/form-server)](https://www.npmjs.com/package/@msbci/form-server)
4
+ [![license](https://img.shields.io/badge/license-Commercial-blue)](./LICENSE.md)
5
+
6
+ REST API server for form management — **Prisma ORM, multi-tenant, PostgreSQL + SQLite**. Compatible with Express, Fastify, and Next.js API Routes.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @msbci/form-server @msbci/form-core @prisma/client
12
+ npm install -D prisma
13
+ ```
14
+
15
+ ## Usage — Next.js API Routes
16
+
17
+ ```typescript
18
+ // app/api/forms/[...path]/route.ts
19
+ import { createFormRouter } from '@msbci/form-server'
20
+ import { PrismaClient } from '@prisma/client'
21
+
22
+ const prisma = new PrismaClient()
23
+ const router = createFormRouter({
24
+ prisma,
25
+ auth: async (req) => {
26
+ const token = req.headers.authorization?.replace('Bearer ', '')
27
+ const user = await verifyToken(token)
28
+ return user ? { userId: user.id, tenantId: user.tenantId } : null
29
+ },
30
+ })
31
+
32
+ export async function GET(req: Request) {
33
+ const url = new URL(req.url)
34
+ const path = url.pathname.replace('/api/forms', '')
35
+ const result = await router.handle({
36
+ method: 'GET',
37
+ path,
38
+ params: {},
39
+ query: Object.fromEntries(url.searchParams),
40
+ body: null,
41
+ headers: Object.fromEntries(req.headers),
42
+ })
43
+ return Response.json(result.body, { status: result.status })
44
+ }
45
+
46
+ export async function POST(req: Request) {
47
+ const url = new URL(req.url)
48
+ const path = url.pathname.replace('/api/forms', '')
49
+ const body = await req.json()
50
+ const result = await router.handle({
51
+ method: 'POST',
52
+ path,
53
+ params: {},
54
+ query: Object.fromEntries(url.searchParams),
55
+ body,
56
+ headers: Object.fromEntries(req.headers),
57
+ })
58
+ return Response.json(result.body, { status: result.status })
59
+ }
60
+ ```
61
+
62
+ ## Usage — Express
63
+
64
+ ```typescript
65
+ import express from 'express'
66
+ import { createFormRouter } from '@msbci/form-server'
67
+ import { PrismaClient } from '@prisma/client'
68
+
69
+ const app = express()
70
+ const router = createFormRouter({ prisma: new PrismaClient() })
71
+
72
+ app.use('/api/forms', express.json(), async (req, res) => {
73
+ const result = await router.handle({
74
+ method: req.method,
75
+ path: req.path,
76
+ params: {},
77
+ query: req.query as Record<string, string>,
78
+ body: req.body,
79
+ headers: req.headers as Record<string, string>,
80
+ })
81
+ res.status(result.status).json(result.body)
82
+ })
83
+ ```
84
+
85
+ ## Auth Injectable
86
+
87
+ The package includes **no auth logic**. Inject your own via the `auth` option:
88
+
89
+ ```typescript
90
+ import type { AuthMiddleware } from '@msbci/form-server'
91
+
92
+ const myAuth: AuthMiddleware = async (req) => {
93
+ const token = req.headers.authorization?.replace('Bearer ', '')
94
+ if (!token) return null // → 401 Unauthorized
95
+ const user = await verifyJWT(token)
96
+ return { userId: user.id, tenantId: user.orgId }
97
+ }
98
+
99
+ createFormRouter({ prisma, auth: myAuth })
100
+ ```
101
+
102
+ Default: `noAuth` — allows all requests (for development).
103
+
104
+ ## Multi-Tenant
105
+
106
+ `tenantId` is an optional field on `FormDefinition` and `FormSubmission`:
107
+
108
+ - Pass `tenantId` when creating forms and submissions
109
+ - Filter by `tenantId` in list queries
110
+ - Auth middleware can set `tenantId` from the authenticated user
111
+
112
+ ## Database
113
+
114
+ | Provider | Use case | URL format |
115
+ |----------|----------|------------|
116
+ | **PostgreSQL** | Production | `postgresql://user:pass@host:5432/db` |
117
+ | **SQLite** | Development / Testing | `file:./dev.db` |
118
+
119
+ Copy the Prisma schema from the package and adjust the `provider`:
120
+
121
+ ```prisma
122
+ datasource db {
123
+ provider = "postgresql" // or "sqlite"
124
+ url = env("DATABASE_URL")
125
+ }
126
+ ```
127
+
128
+ ## Routes (25)
129
+
130
+ | Method | Path | Description |
131
+ |--------|------|-------------|
132
+ | `GET` | `/form-types` | List all form types |
133
+ | `GET` | `/form-types/:id` | Get form type |
134
+ | `POST` | `/form-types` | Create form type |
135
+ | `PUT` | `/form-types/:id` | Update form type |
136
+ | `DELETE` | `/form-types/:id` | Delete form type |
137
+ | `GET` | `/forms` | List forms (paginated, searchable) |
138
+ | `GET` | `/forms/:id` | Get form with full structure |
139
+ | `POST` | `/forms` | Create form |
140
+ | `PUT` | `/forms/:id` | Update form |
141
+ | `DELETE` | `/forms/:id` | Delete form (cascade) |
142
+ | `GET` | `/forms/:id/export` | Export form as JSON |
143
+ | `POST` | `/forms/import` | Import form from JSON |
144
+ | `POST` | `/forms/:formId/pages` | Create page |
145
+ | `PUT` | `/pages/:id` | Update page |
146
+ | `DELETE` | `/pages/:id` | Delete page |
147
+ | `POST` | `/pages/:pageId/rosters` | Create roster |
148
+ | `PUT` | `/rosters/:id` | Update roster |
149
+ | `DELETE` | `/rosters/:id` | Delete roster |
150
+ | `POST` | `/pages/:pageId/variables` | Create variable on page |
151
+ | `POST` | `/rosters/:rosterId/variables` | Create variable on roster |
152
+ | `PUT` | `/variables/:id` | Update variable |
153
+ | `DELETE` | `/variables/:id` | Delete variable |
154
+ | `GET` | `/forms/:formId/submissions` | List submissions (paginated) |
155
+ | `GET` | `/submissions/:id` | Get submission |
156
+ | `POST` | `/submissions` | Create submission |
157
+ | `PUT` | `/submissions/:id` | Update submission status |
158
+
159
+ All inputs validated with Zod. Errors returned as `{ status, message, data, errors }`.
160
+
161
+ ## v1.3.2
162
+
163
+ - **DataSourceConfig CRUD** — nouvelle table `data_source_configs` (`@@unique([code, scopeId])`, `onDelete: Cascade` depuis Scope). 6 nouveaux endpoints REST :
164
+ - `GET / POST /scopes/:scopeId/datasources`
165
+ - `GET / PUT / DELETE /datasources/:id`
166
+ - `GET /forms/:formId/datasources` — **endpoint agrégé** : renvoie `{ items: IDataSourceConfig[], scopeHeaders: IScopeHeaders[] }` (configs + headers décryptés des scopes du form, tous en un seul appel).
167
+ - **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.
168
+ - `MOSOBI_ENCRYPTION_KEY` (32 bytes en base64 ou 64 chars hex) est requis pour **écrire** des headers : `requireEncryption()` lève `503 Service Unavailable` sinon.
169
+ - **Lecture tolérante** : si la clé manque, `decrypt()` renvoie `{}` avec un warning console — la lecture n'est jamais bloquée (la form reste rendable).
170
+ - **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).
171
+ - Suppression d'un scope refusée en **409** si au moins une `DataSourceConfig` le référence (en plus des forms et templates existants).
172
+ - Nouveau helper `ApiError.serviceUnavailable(message)` pour les 503.
173
+ - Migration Postgres `20260524000000_datasource_config` fournie pour `apps/demo`.
174
+ - 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.
175
+ - Tests serveur passent en `fileParallelism: false` pour éviter les courses sur la même `test.db`.
176
+
177
+ ## v1.3.1
178
+
179
+ - **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.
180
+ - `POST /forms` et `PUT /forms/:id` acceptent désormais `scopeIds: string[]` (validé par Zod). Le payload `scopeId` n'est plus reconnu.
181
+ - `GET /forms?scopeId=X` filtre via `scopes: { some: { scopeId } }` (transparent pour le client).
182
+ - `GET /forms/:id` et `GET /forms/:id/resolved` retournent `scopeIds` triés par `order` asc.
183
+ - `dbToFormDefinition` extrait `scopeIds` depuis la relation `scopes` ordonnée.
184
+ - 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.
185
+ - Suppression d'un scope refusée en **409** si au moins une ligne pivot le référence (utilise `formDefinitionScope.count`).
186
+ - 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`.
187
+
188
+ ## v1.3.0
189
+
190
+ - **Scope** : CRUD complet (`/scopes`) + multi-tenant via `tenantId @default("default")`
191
+ - **VariableTemplate** : CRUD complet (`/scopes/:scopeId/templates`, `/templates/:id`) — `code` et `name` immutables (enforced Zod `.strict()` + service)
192
+ - `GET /forms/:id/resolved` — formulaire résolu via Option B (templates appliqués in-memory)
193
+ - 10 nouveaux endpoints REST (`/scopes`, `/scopes/:id`, `/scopes/:scopeId/templates`, `/templates/:id`, `/forms/:id/resolved`)
194
+ - Suppression scope refusée en **409** si forms OU templates rattachés
195
+ - **Tenant `default` créé automatiquement** au boot via `initializeDefaults` (idempotent, `WeakSet<PrismaClient>`)
196
+ - `FormDefinition.scopeId` + `FormVariable.templateId` / `templateOverrides` (champs Prisma)
197
+ - 10 nouveaux tests d'intégration (43 total)
198
+
199
+ ## v1.2.0
200
+
201
+ - `LocalizedString` sérialisé en JSON côté persistence
202
+ - Compatible avec les schémas multilingues `@msbci/form-core` v1.2.0
203
+ - No breaking changes
204
+
205
+ ## What's new in v1.1.0
206
+
207
+ - 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.
208
+ - No breaking change. Drop-in upgrade from v1.0.x.
209
+
210
+ ## License
211
+
212
+ Copyright (c) 2026 MOSOBI — All rights reserved.
213
+ Commercial license required. Contact: dev@mosobi.com