miolo 3.0.0-beta.194 → 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 +5 -5
- package/src/config/defaults.mjs +5 -2
- package/src/engines/schema/diffObjs.mjs +41 -0
- package/src/engines/schema/index.mjs +3 -51
- package/src/engines/schema/input.mjs +52 -0
- package/src/engines/schema/output.mjs +63 -0
- package/src/index.mjs +3 -2
- package/src/middleware/routes/router/queries/attachQueriesRoutes.mjs +97 -11
- package/src/middleware/routes/router/queries/getQueriesConfig.mjs +4 -1
- package/template/.agent/skills/miolo-routing/SKILL.md +14 -7
- package/template/.agent/skills/miolo-schemas/SKILL.md +45 -16
- package/template/package.json +5 -5
- package/template/src/server/db/io/todos/delete.mjs +2 -2
- package/template/src/server/db/io/todos/read.mjs +2 -2
- package/template/src/server/db/io/todos/toggle.mjs +2 -2
- package/template/src/server/db/io/todos/upsave.mjs +2 -2
- package/template/src/server/routes/index.mjs +20 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miolo",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|
package/src/config/defaults.mjs
CHANGED
|
@@ -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:
|
|
227
|
-
|
|
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
|
|
1
|
+
import { with_miolo_input_schema } from "./input.mjs"
|
|
2
|
+
import { with_miolo_output_schema } from "./output.mjs"
|
|
2
3
|
|
|
3
|
-
export
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
const inputSchema = route?.schema?.input
|
|
92
|
+
|
|
93
|
+
if (inputSchema) {
|
|
92
94
|
// Check schema is actually a schema
|
|
93
|
-
if (!Joi.isSchema(
|
|
95
|
+
if (!Joi.isSchema(inputSchema)) {
|
|
94
96
|
ctx.miolo.logger.error(
|
|
95
|
-
`[router] Expecting schema at ${url} but something else was found (${typeof
|
|
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 =
|
|
107
|
+
const v = inputSchema.validate(ctx.request.body)
|
|
106
108
|
|
|
107
109
|
if (v?.error) {
|
|
108
110
|
ctx.miolo.logger.warn(
|
|
109
|
-
`[router]
|
|
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(
|
|
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 =
|
|
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]
|
|
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]
|
|
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:
|
|
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:
|
|
168
|
-
|
|
169
|
-
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
|
10
|
+
## Schema Wrappers: with_miolo_input_schema and with_miolo_output_schema
|
|
11
11
|
|
|
12
|
-
Use `
|
|
12
|
+
Use `with_miolo_input_schema` to wrap route handlers and database functions to validate incoming parameters.
|
|
13
13
|
|
|
14
14
|
```javascript
|
|
15
|
-
import {
|
|
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 =
|
|
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:
|
|
59
|
-
|
|
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 {
|
|
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:
|
|
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 {
|
|
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 =
|
|
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 `
|
|
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
|
|
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
|
|
package/template/package.json
CHANGED
|
@@ -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.
|
|
48
|
-
"miolo-cli": "^3.0.0-beta.
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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:
|
|
36
|
-
|
|
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:
|
|
44
|
-
|
|
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
|
-
|
|
56
|
+
ok: Joi.bool(),
|
|
57
|
+
data: Joi.object({
|
|
58
|
+
id: Joi.number()
|
|
59
|
+
})
|
|
47
60
|
})
|
|
48
61
|
),
|
|
49
62
|
auth
|