miolo 3.0.0-beta.195 → 3.0.0-beta.196

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miolo",
3
- "version": "3.0.0-beta.195",
3
+ "version": "3.0.0-beta.196",
4
4
  "description": "all-in-one koa-based server",
5
5
  "author": "Donato Lorenzo <donato@afialapis.com>",
6
6
  "contributors": [
@@ -47,7 +47,7 @@
47
47
  "@babel/plugin-proposal-decorators": "^7.29.0",
48
48
  "@babel/preset-env": "^7.29.5",
49
49
  "@babel/preset-react": "^7.28.5",
50
- "@dotenvx/dotenvx": "^1.65.0",
50
+ "@dotenvx/dotenvx": "^1.66.0",
51
51
  "@koa/bodyparser": "^6.1.0",
52
52
  "@koa/cors": "^5.0.0",
53
53
  "@koa/router": "^15.5.0",
@@ -60,7 +60,7 @@
60
60
  "@rollup/plugin-replace": "^6.0.3",
61
61
  "@tailwindcss/postcss": "^4.3.0",
62
62
  "@tailwindcss/vite": "^4.3.0",
63
- "@vitejs/plugin-react": "^6.0.1",
63
+ "@vitejs/plugin-react": "^6.0.2",
64
64
  "autoprefixer": "^10.5.0",
65
65
  "cacheiro": "^0.5.0-beta.11",
66
66
  "calustra": "^1.0.0-beta.14",
@@ -86,7 +86,7 @@
86
86
  "nodemailer": "^8.0.7",
87
87
  "passport-google-oauth20": "^2.0.0",
88
88
  "passport-local": "^1.0.0",
89
- "rollup": "^4.60.3",
89
+ "rollup": "^4.60.4",
90
90
  "rollup-plugin-node-externals": "^9.0.1",
91
91
  "rollup-plugin-postcss": "^4.0.2",
92
92
  "socket.io": "^4.8.3",
@@ -94,7 +94,7 @@
94
94
  "statuses": "^2.0.2",
95
95
  "tailwindcss": "^4.3.0",
96
96
  "tinguir": "^0.0.7",
97
- "vite": "^8.0.12",
97
+ "vite": "^8.0.13",
98
98
  "winston": "^3.19.0",
99
99
  "winston-daily-rotate-file": "^5.0.0",
100
100
  "yargs-parser": "^22.0.0"
@@ -223,8 +223,11 @@ export default function make_config_defaults() {
223
223
  auth: ...,
224
224
  before: async (ctx) => { return true/false } or array of functions,
225
225
  after : async (ctx, data) => { return data } or array of functions,
226
- schema: a Joi schema,
227
- keep_body: false by default. If true, miolo wont wnsure ctx.body after callback.
226
+ schema: {
227
+ input: a Joi schema for validating input data,
228
+ output: a Joi schema for validating output data,
229
+ }
230
+ keep_body: false by default. If true, miolo wont ensure ctx.body after callback.
228
231
  },
229
232
  ],
230
233
  },
@@ -0,0 +1,41 @@
1
+ export function diffObjs(o1, o2) {
2
+ const missingKeys = []
3
+
4
+ function findMissing(obj1, obj2, prefix = "") {
5
+ for (const key in obj1) {
6
+ if (Object.hasOwn(obj1, key)) {
7
+ const fullKey = prefix ? `${prefix}.${key}` : key
8
+
9
+ if (!obj2 || !(key in obj2)) {
10
+ missingKeys.push(`'${fullKey}'`)
11
+ } else if (
12
+ typeof obj1[key] === "object" &&
13
+ obj1[key] !== null &&
14
+ typeof obj2[key] === "object" &&
15
+ obj2[key] !== null &&
16
+ !Array.isArray(obj1[key]) &&
17
+ !Array.isArray(obj2[key])
18
+ ) {
19
+ findMissing(obj1[key], obj2[key], fullKey)
20
+ }
21
+ }
22
+ }
23
+ }
24
+
25
+ findMissing(o1, o2)
26
+
27
+ if (missingKeys.length === 0) {
28
+ return ""
29
+ }
30
+
31
+ const isPlural = missingKeys.length > 1
32
+ let keysStr = ""
33
+ if (!isPlural) {
34
+ keysStr = missingKeys[0]
35
+ } else {
36
+ const lastKey = missingKeys.pop()
37
+ keysStr = `${missingKeys.join(", ")} and ${lastKey}`
38
+ }
39
+
40
+ return keysStr
41
+ }
@@ -1,52 +1,4 @@
1
- import Joi from "joi"
1
+ import { with_miolo_input_schema } from "./input.mjs"
2
+ import { with_miolo_output_schema } from "./output.mjs"
2
3
 
3
- export function with_miolo_schema(fn, schema) {
4
- return async (ctx, params) => {
5
- let error
6
-
7
- // Check schema is actually a schema
8
- if (!schema || !Joi.isSchema(schema)) {
9
- error = `Expecting schema for ${fn.name} but something else was found (${typeof schema})`
10
- ctx.miolo.logger.silly(`[validation][${fn.name}] ${error}`)
11
- throw new Error(error)
12
- }
13
-
14
- // perform validation
15
- let v
16
- try {
17
- v = schema.validate(params)
18
- } catch (uerror) {
19
- error = `Unexpected error validating data for ${fn.name}: ${uerror?.message || uerror}`
20
- ctx.miolo.logger.silly(`[validation][${fn.name}] ${error}`)
21
- throw new Error(error)
22
- }
23
-
24
- // raise validation errors
25
- if (v?.error) {
26
- error = `Schema invalidated data for ${fn.name}: ${v.error}\n${v.error.annotate(true)}`
27
- ctx.miolo.logger.silly(`[validation][${fn.name}] ${error}`)
28
- throw new Error(error)
29
- }
30
-
31
- // check parsed value is ok
32
- if (!v?.value) {
33
- const description = schema.describe()
34
-
35
- // Check if schema was deliberately set to allow only null
36
- // schema = Joi.any().allow(null)
37
- const isOnlyNull =
38
- description.type === "any" &&
39
- description.allow &&
40
- description.allow.length === 1 &&
41
- description.allow[0] === null
42
-
43
- if (!isOnlyNull) {
44
- error = `Schema returned unknown result for ${fn.name}: ${JSON.stringify(v)}`
45
- ctx.miolo.logger.silly(`[validation][${fn.name}] ${error}`)
46
- throw new Error(error)
47
- }
48
- }
49
-
50
- return await fn(ctx, v.value)
51
- }
52
- }
4
+ export { with_miolo_input_schema, with_miolo_output_schema }
@@ -0,0 +1,52 @@
1
+ import Joi from "joi"
2
+
3
+ export function with_miolo_input_schema(fn, schema) {
4
+ return async (ctx, params) => {
5
+ let error
6
+
7
+ // Check schema is actually a schema
8
+ if (!schema || !Joi.isSchema(schema)) {
9
+ error = `Expecting input schema for ${fn.name} but something else was found (${typeof schema})`
10
+ ctx.miolo.logger.silly(`[validation][${fn.name}] ${error}`)
11
+ throw new Error(error)
12
+ }
13
+
14
+ // perform validation
15
+ let v
16
+ try {
17
+ v = schema.validate(params)
18
+ } catch (uerror) {
19
+ error = `Unexpected error validating input data for ${fn.name}: ${uerror?.message || uerror}`
20
+ ctx.miolo.logger.silly(`[validation][${fn.name}] ${error}`)
21
+ throw new Error(error)
22
+ }
23
+
24
+ // raise validation errors
25
+ if (v?.error) {
26
+ error = `Input schema invalidated data for ${fn.name}: ${v.error}\n${v.error.annotate(true)}`
27
+ ctx.miolo.logger.silly(`[validation][${fn.name}] ${error}`)
28
+ throw new Error(error)
29
+ }
30
+
31
+ // check parsed value is ok
32
+ if (!v?.value) {
33
+ const description = schema.describe()
34
+
35
+ // Check if schema was deliberately set to allow only null
36
+ // schema = Joi.any().allow(null)
37
+ const isOnlyNull =
38
+ description.type === "any" &&
39
+ description.allow &&
40
+ description.allow.length === 1 &&
41
+ description.allow[0] === null
42
+
43
+ if (!isOnlyNull) {
44
+ error = `Input schema returned unknown result for ${fn.name}: ${JSON.stringify(v)}`
45
+ ctx.miolo.logger.silly(`[validation][${fn.name}] ${error}`)
46
+ throw new Error(error)
47
+ }
48
+ }
49
+
50
+ return await fn(ctx, v.value)
51
+ }
52
+ }
@@ -0,0 +1,63 @@
1
+ import Joi from "joi"
2
+ import { diffObjs } from "./diffObjs.mjs"
3
+
4
+ export function with_miolo_output_schema(fn, schema) {
5
+ const fnName = fn?.name ? `[${fn.name}]` : ""
6
+
7
+ return async (ctx, params) => {
8
+ let error
9
+
10
+ // Check schema is actually a schema
11
+ if (!schema || !Joi.isSchema(schema)) {
12
+ error = `Expecting output schema for ${fn.name} but something else was found (${typeof schema})`
13
+ ctx.miolo.logger.silly(`[validation]${fnName} ${error}`)
14
+ throw new Error(error)
15
+ }
16
+
17
+ // Call the function first
18
+ const result = await fn(ctx, params)
19
+
20
+ // perform validation over the result
21
+ let v
22
+ try {
23
+ v = schema.validate(result, { stripUnknown: true })
24
+ } catch (uerror) {
25
+ error = `Unexpected error validating output data for ${fn.name}: ${uerror?.message || uerror}`
26
+ ctx.miolo.logger.silly(`[validation]${fnName} ${error}`)
27
+ throw new Error(error)
28
+ }
29
+
30
+ // raise validation errors
31
+ if (v?.error) {
32
+ error = `Output schema invalidated data for ${fn.name}: ${v.error}\n${v.error.annotate(true)}`
33
+ ctx.miolo.logger.silly(`[validation]${fnName} ${error}`)
34
+ throw new Error(error)
35
+ }
36
+
37
+ // check parsed value is ok
38
+ if (!v?.value) {
39
+ const description = schema.describe()
40
+
41
+ // Check if schema was deliberately set to allow only null
42
+ // schema = Joi.any().allow(null)
43
+ const isOnlyNull =
44
+ description.type === "any" &&
45
+ description.allow &&
46
+ description.allow.length === 1 &&
47
+ description.allow[0] === null
48
+
49
+ if (!isOnlyNull) {
50
+ error = `Output schema returned unknown result for ${fn.name}: ${JSON.stringify(v)}`
51
+ ctx.miolo.logger.silly(`[validation]${fnName} ${error}`)
52
+ throw new Error(error)
53
+ }
54
+ }
55
+
56
+ const diff = diffObjs(result, v.value)
57
+ if (diff) {
58
+ ctx.miolo.logger.debug(`[validation]${fnName} Output schema has removed ${diff}`)
59
+ }
60
+
61
+ return v.value
62
+ }
63
+ }
package/src/index.mjs CHANGED
@@ -3,7 +3,7 @@ import { miolo_db_connection_pg, miolo_db_drop_connections } from "./db-conn.mjs
3
3
  import { init_emailer_transporter as miolo_emailer } from "./engines/emailer/index.mjs"
4
4
  import { init_logger as miolo_logger } from "./engines/logger/index.mjs"
5
5
  import { init_parser as miolo_parser } from "./engines/parser/index.mjs"
6
- import { with_miolo_schema } from "./engines/schema/index.mjs"
6
+ import { with_miolo_input_schema, with_miolo_output_schema } from "./engines/schema/index.mjs"
7
7
  import { miolo } from "./server.mjs"
8
8
  import { miolo_cron } from "./server-cron.mjs"
9
9
 
@@ -18,5 +18,6 @@ export {
18
18
  miolo_emailer,
19
19
  miolo_logger,
20
20
  miolo_parser,
21
- with_miolo_schema
21
+ with_miolo_input_schema,
22
+ with_miolo_output_schema
22
23
  }
@@ -88,25 +88,27 @@ function attachQueriesRoutes(router, queriesConfigs, logger) {
88
88
  }
89
89
  }
90
90
 
91
- if (route?.schema) {
91
+ const inputSchema = route?.schema?.input
92
+
93
+ if (inputSchema) {
92
94
  // Check schema is actually a schema
93
- if (!Joi.isSchema(route.schema)) {
95
+ if (!Joi.isSchema(inputSchema)) {
94
96
  ctx.miolo.logger.error(
95
- `[router] Expecting schema at ${url} but something else was found (${typeof route.schema})`
97
+ `[router] Expecting input schema at ${url} but something else was found (${typeof inputSchema})`
96
98
  )
97
99
  ctx.body = {
98
100
  ok: false,
99
- error: "Invalid schema"
101
+ error: "Invalid input schema"
100
102
  }
101
103
  return
102
104
  }
103
105
 
104
106
  try {
105
- const v = route.schema.validate(ctx.request.body)
107
+ const v = inputSchema.validate(ctx.request.body)
106
108
 
107
109
  if (v?.error) {
108
110
  ctx.miolo.logger.warn(
109
- `[router] Schema invalidated data for ${url}: ${v.error}\n${v.error.annotate(true)}`
111
+ `[router] Input schema invalidated data for ${url}: ${v.error}\n${v.error.annotate(true)}`
110
112
  )
111
113
  ctx.body = {
112
114
  ok: false,
@@ -114,10 +116,12 @@ function attachQueriesRoutes(router, queriesConfigs, logger) {
114
116
  }
115
117
  return
116
118
  } else if (v?.value) {
117
- ctx.miolo.logger.silly(`[router] Schema validated data for ${url} successfully`)
119
+ ctx.miolo.logger.silly(
120
+ `[router] Input schema validated data for ${url} successfully`
121
+ )
118
122
  ctx.request.body = v.value
119
123
  } else {
120
- const description = route.schema.describe()
124
+ const description = inputSchema.describe()
121
125
 
122
126
  // Check if schema was deliberately set to allow only null
123
127
  // schema = Joi.any().allow(null)
@@ -129,18 +133,18 @@ function attachQueriesRoutes(router, queriesConfigs, logger) {
129
133
 
130
134
  if (isOnlyNull) {
131
135
  ctx.miolo.logger.silly(
132
- `[router] Schema allowed null param to ${url} successfully`
136
+ `[router] Input schema allowed null param to ${url} successfully`
133
137
  )
134
138
  ctx.request.body = v.value
135
139
  } else {
136
140
  ctx.miolo.logger.warn(
137
- `[router] Schema returned unknown result for ${url}: ${JSON.stringify(v)}. Let's ignore it.`
141
+ `[router] Input schema returned unknown result for ${url}: ${JSON.stringify(v)}. Let's ignore it.`
138
142
  )
139
143
  }
140
144
  }
141
145
  } catch (error) {
142
146
  ctx.miolo.logger.error(
143
- `[router] Error validating schema at ${url}: ${error?.message || error}`
147
+ `[router] Error validating input schema at ${url}: ${error?.message || error}`
144
148
  )
145
149
  ctx.body = {
146
150
  ok: false,
@@ -164,6 +168,88 @@ function attachQueriesRoutes(router, queriesConfigs, logger) {
164
168
  const result = await route.after(ctx, ctx.body)
165
169
  ctx.body = ensure_response_is_ok_data(ctx, result)
166
170
  }
171
+
172
+ // If body has no ok and read data, do not validate it
173
+ if (ctx.body?.ok !== true || !ctx.body?.data) {
174
+ return
175
+ }
176
+
177
+ const outputSchema = route?.schema?.output
178
+
179
+ if (outputSchema) {
180
+ // Check schema is actually a schema
181
+ if (!Joi.isSchema(outputSchema)) {
182
+ ctx.miolo.logger.error(
183
+ `[router] Expecting output schema at ${url} but something else was found (${typeof outputSchema})`
184
+ )
185
+ ctx.body = {
186
+ ok: false,
187
+ error: "Invalid output schema"
188
+ }
189
+ return
190
+ }
191
+
192
+ // perform validation over the result
193
+ let v
194
+ try {
195
+ v = outputSchema.validate(ctx.body.data, { stripUnknown: true })
196
+ } catch (uerror) {
197
+ const error = `Unexpected error validating output data for ${url}: ${uerror?.message || uerror}`
198
+ ctx.miolo.logger.warn(`[router] ${error}`)
199
+ ctx.body = {
200
+ ok: false,
201
+ error: error
202
+ }
203
+ return
204
+ }
205
+
206
+ // raise validation errors
207
+ if (v?.error) {
208
+ ctx.miolo.logger.warn(
209
+ `[router] Output schema invalidated data for ${url}: ${v.error}\n${v.error.annotate(true)}`
210
+ )
211
+ ctx.body = {
212
+ ok: false,
213
+ error: v.error.toString()
214
+ }
215
+ return
216
+ }
217
+
218
+ if (v?.value) {
219
+ const diff = diffObjs(ctx.body.data, v.value)
220
+ if (diff) {
221
+ ctx.miolo.logger.debug(`[router] Output schema has removed ${diff} for URL ${url}`)
222
+ }
223
+
224
+ ctx.miolo.logger.silly(
225
+ `[router] Output schema validated data for ${url} successfully`
226
+ )
227
+ ctx.body.data = v.value
228
+ }
229
+
230
+ // check parsed value is ok
231
+ if (!v?.value) {
232
+ const description = outputSchema.describe()
233
+
234
+ // Check if schema was deliberately set to allow only null
235
+ // schema = Joi.any().allow(null)
236
+ const isOnlyNull =
237
+ description.type === "any" &&
238
+ description.allow &&
239
+ description.allow.length === 1 &&
240
+ description.allow[0] === null
241
+
242
+ if (!isOnlyNull) {
243
+ const error = `Output schema returned unknown result for ${fn.name}: ${JSON.stringify(v)}`
244
+ ctx.miolo.logger.silly(`[router][${fn.name}] ${error}`)
245
+ ctx.body = {
246
+ ok: false,
247
+ error: error
248
+ }
249
+ return
250
+ }
251
+ }
252
+ }
167
253
  } catch (error) {
168
254
  ctx.miolo.logger.error(
169
255
  `[router] Unexpected error on Query ${route.callback?.name} at ${url}`
@@ -27,7 +27,10 @@ import { DEFAULT_AUTH_USER } from "../defaults.mjs"
27
27
  auth,
28
28
  before: async (ctx) => { return true/false } or array of functions,
29
29
  after : async (ctx, data) => { return data } or array of functions,
30
- schema: a Joi schema,
30
+ schema: {
31
+ input: a Joi schema for validating input data,
32
+ output: a Joi schema for validating output data,
33
+ },
31
34
  keep_body: false by default. If true, miolo wont wnsure ctx.body after callback.
32
35
  }
33
36
  ]
@@ -164,10 +164,17 @@ export default [{
164
164
  url: '/item/search',
165
165
  auth,
166
166
  callback: r_item_search,
167
- schema: Joi.object({
168
- query: Joi.string().min(3).required(),
169
- limit: Joi.number().min(1).max(100).default(20)
170
- })
167
+ schema: {
168
+ input: Joi.object({
169
+ query: Joi.string().min(3).required(),
170
+ limit: Joi.number().min(1).max(100).default(20)
171
+ }),
172
+ output: Joi.object({
173
+ ok: Joi.boolean().required(),
174
+ data: Joi.array().items(Joi.object()).required(),
175
+ error: Joi.string().optional()
176
+ })
177
+ }
171
178
  }
172
179
  ]
173
180
  }]
@@ -175,7 +182,7 @@ export default [{
175
182
 
176
183
  **Wrapper function:**
177
184
  ```javascript
178
- import { with_miolo_schema } from 'miolo'
185
+ import { with_miolo_input_schema } from 'miolo'
179
186
  import Joi from 'joi'
180
187
 
181
188
  const itemSchema = Joi.object({
@@ -190,7 +197,7 @@ export default [{
190
197
  method: 'POST',
191
198
  url: '/item/save',
192
199
  auth,
193
- callback: with_miolo_schema(r_item_upsave, itemSchema)
200
+ callback: with_miolo_input_schema(r_item_upsave, itemSchema)
194
201
  }
195
202
  ]
196
203
  }]
@@ -305,7 +312,7 @@ export async function r_item_find(ctx, params) {
305
312
  2. **Consistent naming** - Routes start with `r_`, database functions with `db_`
306
313
  3. **Always log** - Use `ctx.miolo.logger` for all operations (`info`level)
307
314
  4. **Return consistent format** - Always `{ ok, data }` or `{ ok, error }`
308
- 5. **Validate inputs** - Use Joi schemas for validation
315
+ 5. **Validate inputs (and outputs)** - Use Joi schemas for validation
309
316
  6. **Handle errors** - Wrap in try/catch, return meaningful errors
310
317
  7. **Check user access** - Use `ctx.state.user` to verify permissions
311
318
  8. **Keep handlers thin** - Business logic in `db/io/`, routes only handle HTTP
@@ -1,18 +1,18 @@
1
1
  ---
2
2
  name: miolo-schemas
3
- description: Joi validation schemas and patterns for miolo applications. Use when implementing request/parameter validation for routes or database functions, creating reusable schema components, or using with_miolo_schema wrapper.
3
+ description: Joi validation schemas and patterns for miolo applications. Use when implementing request/parameter validation for routes or database functions, creating reusable schema components, or using with_miolo_input_schema and with_miolo_output_schema wrappers.
4
4
  ---
5
5
 
6
6
  # Miolo Validation Schemas
7
7
 
8
8
  Joi validation schema patterns for miolo routes and database functions.
9
9
 
10
- ## Schema Wrapper: with_miolo_schema
10
+ ## Schema Wrappers: with_miolo_input_schema and with_miolo_output_schema
11
11
 
12
- Use `with_miolo_schema` to wrap route handlers and database functions with validation:
12
+ Use `with_miolo_input_schema` to wrap route handlers and database functions to validate incoming parameters.
13
13
 
14
14
  ```javascript
15
- import { with_miolo_schema } from 'miolo'
15
+ import { with_miolo_input_schema } from 'miolo'
16
16
  import Joi from 'joi'
17
17
 
18
18
  // Define schema
@@ -29,17 +29,39 @@ async function _r_todo_upsave(ctx, params) {
29
29
  // ...
30
30
  }
31
31
 
32
- export const r_todo_upsave = with_miolo_schema(_r_todo_upsave, todoSchema)
32
+ export const r_todo_upsave = with_miolo_input_schema(_r_todo_upsave, todoSchema)
33
33
  ```
34
34
 
35
35
  **Key benefits:**
36
- - Automatic validation before function execution
36
+ - Automatic validation before function execution (input) or before sending the response (output)
37
37
  - Validation errors handled automatically
38
38
  - Default values applied
39
39
  - Type coercion
40
+ - Extraneous fields removed (diff log generated on output schemas)
41
+
42
+ ### Using `with_miolo_output_schema`
43
+
44
+ Use `with_miolo_output_schema` to ensure that data returned by a function matches the expected format, automatically stripping any extra properties not defined in the schema.
45
+
46
+ ```javascript
47
+ import { with_miolo_output_schema } from 'miolo'
48
+ import Joi from 'joi'
49
+
50
+ const todoOutputSchema = Joi.object({
51
+ ok: Joi.boolean().required(),
52
+ data: Joi.object({
53
+ id: Joi.number().required(),
54
+ description: Joi.string().required()
55
+ })
56
+ })
57
+
58
+ export const db_todo_insert = with_miolo_output_schema(_db_todo_insert, todoOutputSchema)
59
+ ```
40
60
 
41
61
  ## Using Schemas in Routes
42
62
 
63
+ Route definitions accept a `schema` object that can contain both `input` and `output` schemas:
64
+
43
65
  ### Inline Schema (Route Definition)
44
66
 
45
67
  Define schema directly in route configuration:
@@ -55,9 +77,15 @@ export default [{
55
77
  url: '/todo/last_hours',
56
78
  auth,
57
79
  callback: r_todo_count_last_hours,
58
- schema: Joi.object({
59
- hours: Joi.number().min(1).max(24).required()
60
- })
80
+ schema: {
81
+ input: Joi.object({
82
+ hours: Joi.number().min(1).max(24).required()
83
+ }),
84
+ output: Joi.object({
85
+ ok: Joi.boolean().required(),
86
+ data: Joi.number().required()
87
+ })
88
+ }
61
89
  }
62
90
  ]
63
91
  }]
@@ -68,7 +96,7 @@ export default [{
68
96
  Wrap the handler function with schema:
69
97
 
70
98
  ```javascript
71
- import { with_miolo_schema } from 'miolo'
99
+ import { with_miolo_input_schema } from 'miolo'
72
100
  import Joi from 'joi'
73
101
 
74
102
  const todoSchema = Joi.object({
@@ -82,7 +110,7 @@ export default [{
82
110
  method: 'POST',
83
111
  url: '/todo/fake',
84
112
  auth,
85
- callback: with_miolo_schema(r_todo_insert_fake, todoSchema)
113
+ callback: with_miolo_input_schema(r_todo_insert_fake, todoSchema)
86
114
  }
87
115
  ]
88
116
  }]
@@ -93,7 +121,7 @@ export default [{
93
121
  **Strongly recommended** to validate database function parameters:
94
122
 
95
123
  ```javascript
96
- import { with_miolo_schema } from 'miolo'
124
+ import { with_miolo_input_schema } from 'miolo'
97
125
  import Joi from 'joi'
98
126
  import { opt_int, bool_null, opt_str_null } from '#server/utils/schema.mjs'
99
127
 
@@ -118,13 +146,13 @@ const todo_read_schema = Joi.object({
118
146
  })
119
147
 
120
148
  // Export wrapped function
121
- export const db_todo_read = with_miolo_schema(_db_todo_read, todo_read_schema)
149
+ export const db_todo_read = with_miolo_input_schema(_db_todo_read, todo_read_schema)
122
150
  ```
123
151
 
124
152
  **Pattern:**
125
153
  1. Create private implementation function with `_` prefix
126
154
  2. Define schema using partial schemas from `utils/schema.mjs`
127
- 3. Export wrapped function with `with_miolo_schema`
155
+ 3. Export wrapped function with `with_miolo_input_schema`
128
156
 
129
157
  ## Partial Schemas (Reusable Components)
130
158
 
@@ -254,14 +282,15 @@ const schema = Joi.object({
254
282
 
255
283
  ## Best Practices
256
284
 
257
- 1. **Always validate route parameters** - Use schema in route definition or with_miolo_schema
258
- 2. **Strongly recommended for db functions** - Wrap all db_*() functions with schemas
285
+ 1. **Always validate route parameters and outputs** - Use `schema: { input, output }` in route definition.
286
+ 2. **Strongly recommended for db functions** - Wrap all db_*() functions with schemas using `with_miolo_input_schema` or `with_miolo_output_schema`.
259
287
  3. **Use partial schemas** - Reuse common patterns from `utils/schema.mjs`
260
288
  4. **Add to partial schemas** - Extend `utils/schema.mjs` with new reusable patterns
261
289
  5. **Private + public pattern** - Use `_function_name` for implementation, export wrapped version
262
290
  6. **Default values** - Use `.default()` for optional fields with sensible defaults
263
291
  7. **Allow null carefully** - Only use `.allow(null)` when null is a valid business value
264
292
  8. **Validate early** - Validation should happen before any business logic
293
+ 9. **Remove unused properties** - `with_miolo_output_schema` ensures the client doesn't get extraneous fields.
265
294
 
266
295
  ## Extending Partial Schemas
267
296
 
@@ -44,15 +44,15 @@
44
44
  "farrapa": "^3.0.0-beta.10",
45
45
  "intre": "^3.0.0-beta.4",
46
46
  "joi": "^18.2.1",
47
- "lucide-react": "^1.14.0",
48
- "miolo-cli": "^3.0.0-beta.195",
47
+ "lucide-react": "^1.16.0",
48
+ "miolo-cli": "^3.0.0-beta.196",
49
49
  "miolo-model": "file:../miolo-model",
50
- "miolo-react": "^3.0.0-beta.195",
50
+ "miolo-react": "^3.0.0-beta.196",
51
51
  "next-themes": "^0.4.6",
52
52
  "radix-ui": "^1.4.3",
53
53
  "react": "^19.2.6",
54
54
  "react-dom": "^19.2.6",
55
- "react-router": "^7.15.0",
55
+ "react-router": "^7.15.1",
56
56
  "recharts": "^3.8.1",
57
57
  "sonner": "^2.0.7",
58
58
  "tailwind": "^4.0.0",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "@biomejs/biome": "2.4.15",
65
- "miolo": "^3.0.0-beta.195",
65
+ "miolo": "^3.0.0-beta.196",
66
66
  "sass-embedded": "^1.99.0"
67
67
  },
68
68
  "overrides": {
@@ -1,5 +1,5 @@
1
1
  import Joi from "joi"
2
- import { with_miolo_schema } from "miolo"
2
+ import { with_miolo_input_schema } from "miolo"
3
3
  import { opt_int } from "#server/utils/schema.mjs"
4
4
  import { db_todo_find } from "./find.mjs"
5
5
 
@@ -26,4 +26,4 @@ const todo_delete_schema = Joi.object({
26
26
  id: opt_int
27
27
  })
28
28
 
29
- export const db_todo_delete = with_miolo_schema(_db_todo_delete, todo_delete_schema)
29
+ export const db_todo_delete = with_miolo_input_schema(_db_todo_delete, todo_delete_schema)
@@ -1,5 +1,5 @@
1
1
  import Joi from "joi"
2
- import { with_miolo_schema } from "miolo"
2
+ import { with_miolo_input_schema } from "miolo"
3
3
  import { make_query_filter } from "#server/db/io/filter.mjs"
4
4
  import { bool_null, opt_int, opt_str_null } from "#server/utils/schema.mjs"
5
5
 
@@ -80,4 +80,4 @@ const todo_read_schema = Joi.object({
80
80
  })
81
81
  })
82
82
 
83
- export const db_todo_read = with_miolo_schema(_db_todo_read, todo_read_schema)
83
+ export const db_todo_read = with_miolo_input_schema(_db_todo_read, todo_read_schema)
@@ -1,5 +1,5 @@
1
1
  import Joi from "joi"
2
- import { with_miolo_schema } from "miolo"
2
+ import { with_miolo_input_schema } from "miolo"
3
3
  import { bool_null, opt_int } from "#server/utils/schema.mjs"
4
4
  import { db_todo_find } from "./find.mjs"
5
5
  import { db_todo_upsave } from "./upsave.mjs"
@@ -34,4 +34,4 @@ const todo_toggle_schema = Joi.object({
34
34
  done: bool_null
35
35
  })
36
36
 
37
- export const db_todo_toggle = with_miolo_schema(_db_todo_toggle, todo_toggle_schema)
37
+ export const db_todo_toggle = with_miolo_input_schema(_db_todo_toggle, todo_toggle_schema)
@@ -1,5 +1,5 @@
1
1
  import Joi from "joi"
2
- import { with_miolo_schema } from "miolo"
2
+ import { with_miolo_input_schema } from "miolo"
3
3
  import { bool_null, opt_int, opt_str_null } from "#server/utils/schema.mjs"
4
4
 
5
5
  async function _db_todo_upsave(ctx, params) {
@@ -29,4 +29,4 @@ const todo_upsave_schema = Joi.object({
29
29
  done: bool_null
30
30
  })
31
31
 
32
- export const db_todo_upsave = with_miolo_schema(_db_todo_upsave, todo_upsave_schema)
32
+ export const db_todo_upsave = with_miolo_input_schema(_db_todo_upsave, todo_upsave_schema)
@@ -1,5 +1,5 @@
1
1
  import Joi from "joi"
2
- import { with_miolo_schema } from "miolo"
2
+ import { with_miolo_input_schema, with_miolo_output_schema } from "miolo"
3
3
  import { r_todo_delete, r_todo_toggle_done, r_todo_upsave } from "./todos/mod.mjs"
4
4
 
5
5
  import { r_todo_find, r_todo_last, r_todo_list } from "./todos/read.mjs"
@@ -32,18 +32,31 @@ export default [
32
32
  method: "GET",
33
33
  // Passing schema on the route definition
34
34
  callback: r_todo_count_last_hours,
35
- schema: Joi.object({
36
- hours: Joi.number().min(1).max(24)
37
- })
35
+ schema: {
36
+ input: Joi.object({
37
+ hours: Joi.number().min(1).max(24)
38
+ }),
39
+ output: Joi.object({
40
+ count: Joi.number()
41
+ })
42
+ }
38
43
  },
39
44
  {
40
45
  url: "/todo/fake",
41
46
  method: "POST",
42
47
  // Wrapping function with the schema
43
- callback: with_miolo_schema(
44
- r_todo_insert_fake,
48
+ callback: with_miolo_output_schema(
49
+ with_miolo_input_schema(
50
+ r_todo_insert_fake,
51
+ Joi.object({
52
+ done: Joi.bool().optional().default(false)
53
+ })
54
+ ),
45
55
  Joi.object({
46
- done: Joi.bool().optional().default(false)
56
+ ok: Joi.bool(),
57
+ data: Joi.object({
58
+ id: Joi.number()
59
+ })
47
60
  })
48
61
  ),
49
62
  auth