@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 +7 -7
- package/README.md +213 -213
- package/dist/index.cjs +1 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +65 -55
- package/dist/index.d.ts +65 -55
- package/dist/index.mjs +1 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -18
- package/src/prisma/schema.prisma +286 -286
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
|
-
[](https://www.npmjs.com/package/@msbci/form-server)
|
|
4
|
-
[](./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
|
+
[](https://www.npmjs.com/package/@msbci/form-server)
|
|
4
|
+
[](./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
|