codehooks-js 1.3.25 → 1.4.1
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/README.md +28 -0
- package/crudlify/index.mjs +4 -0
- package/crudlify/lib/schema/json-schema/index.mjs +35 -19
- package/index.js +76 -7
- package/openapi/crudlify-docs.mjs +823 -0
- package/openapi/generator.mjs +417 -0
- package/openapi/index.mjs +221 -0
- package/openapi/schema-converter.mjs +668 -0
- package/openapi/swagger-ui.mjs +92 -0
- package/package.json +12 -3
- package/types/index.d.ts +256 -0
package/README.md
CHANGED
|
@@ -128,6 +128,33 @@ app.crudlify();
|
|
|
128
128
|
export default app.init();
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
## OpenAPI Documentation
|
|
132
|
+
|
|
133
|
+
Automatically generate interactive API documentation with Swagger UI:
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
import { app } from 'codehooks-js';
|
|
137
|
+
import { z } from 'zod';
|
|
138
|
+
|
|
139
|
+
const userSchema = z.object({
|
|
140
|
+
name: z.string().min(1).describe('User name'),
|
|
141
|
+
email: z.string().email().describe('Email address'),
|
|
142
|
+
role: z.enum(['admin', 'user']).default('user')
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
app.openapi({
|
|
146
|
+
info: { title: 'My API', version: '1.0.0' }
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.crudlify({ users: userSchema });
|
|
150
|
+
|
|
151
|
+
export default app.init();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
After deploying, visit `/docs` for Swagger UI or `/openapi.json` for the spec.
|
|
155
|
+
|
|
156
|
+
Supports **Zod**, **Yup**, and **JSON Schema** for validation and documentation. See [OpenAPI Documentation](https://codehooks.io/docs/openapi-swagger-docs) for details.
|
|
157
|
+
|
|
131
158
|
## Compile
|
|
132
159
|
|
|
133
160
|
When running the `coho compile` command, it will automatically create a `tsconfig.json` file in the project directory. The tsconfig file can be further adapted to your needs, the initial configuration is shown in the example below:
|
|
@@ -212,6 +239,7 @@ The Codehooks class provides a comprehensive backend application framework with
|
|
|
212
239
|
- **`set(key, val)`** - Set application configuration settings
|
|
213
240
|
- **`render(view, data, cb)`** - Render templates with data
|
|
214
241
|
- **`crudlify(schema, options)`** - Auto-generate CRUD REST API endpoints
|
|
242
|
+
- **`openapi(config, uiPath)`** - Enable OpenAPI/Swagger documentation at `/docs` and `/openapi.json`
|
|
215
243
|
|
|
216
244
|
### **Real-time Communication APIs**
|
|
217
245
|
|
package/crudlify/index.mjs
CHANGED
|
@@ -20,6 +20,10 @@ export default async function crudlify(app, schema = {}, options = { schema: "yu
|
|
|
20
20
|
if (_opt.prefix === undefined || _opt.prefix === '/') {
|
|
21
21
|
_opt.prefix = '';
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
// Store schema info for OpenAPI documentation
|
|
25
|
+
app.set('_crudlify_schema', schema);
|
|
26
|
+
app.set('_crudlify_options', _opt);
|
|
23
27
|
|
|
24
28
|
try {
|
|
25
29
|
// the DB variable is present when running as a codehooks.io app
|
|
@@ -1,41 +1,57 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// JSON Schema validation using ajv (available in Codehooks runtime)
|
|
2
|
+
import Ajv from 'ajv';
|
|
3
|
+
|
|
4
|
+
let ajv = null;
|
|
5
|
+
let compiledSchemas = {};
|
|
3
6
|
const debug = console.debug;
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
|
-
*
|
|
7
|
-
* @param {*} schema
|
|
8
|
-
* @param {*} document
|
|
9
|
-
* @
|
|
9
|
+
* Validate a document against a JSON Schema
|
|
10
|
+
* @param {*} schema
|
|
11
|
+
* @param {*} document
|
|
12
|
+
* @param {*} options
|
|
13
|
+
* @returns
|
|
10
14
|
*/
|
|
11
15
|
export const validate = (schema, document, options) => {
|
|
12
16
|
debug('Validate JSON-schema', document, schema)
|
|
13
17
|
return new Promise((resolve, reject) => {
|
|
14
|
-
|
|
18
|
+
if (!ajv) {
|
|
19
|
+
reject(new Error('No JSON Schema validator available'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Get or compile the validator for this schema
|
|
24
|
+
const schemaKey = JSON.stringify(schema);
|
|
25
|
+
let validateFn = compiledSchemas[schemaKey];
|
|
26
|
+
if (!validateFn) {
|
|
27
|
+
try {
|
|
28
|
+
validateFn = ajv.compile(schema);
|
|
29
|
+
compiledSchemas[schemaKey] = validateFn;
|
|
30
|
+
} catch (e) {
|
|
31
|
+
reject(new Error(`Invalid JSON Schema: ${e.message}`));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const valid = validateFn(document);
|
|
15
37
|
debug('isValid', valid)
|
|
16
38
|
if (!valid) {
|
|
17
|
-
reject(
|
|
39
|
+
reject(validateFn.errors);
|
|
18
40
|
} else {
|
|
19
41
|
resolve(document);
|
|
20
42
|
}
|
|
21
43
|
})
|
|
22
44
|
}
|
|
23
45
|
|
|
24
|
-
/*
|
|
25
|
-
var validator = new ZSchema();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// now validate our data against the last schema
|
|
29
|
-
var valid = validator.validate(data, schemas[2]);
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
46
|
export const cast = (schema, document) => {
|
|
33
47
|
debug('Cast', document, schema)
|
|
34
48
|
return document;
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
export const prepare = (schemas, options) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
// Use provided ajv instance or create default
|
|
53
|
+
ajv = options?.validator || new Ajv({ allErrors: true, strict: false });
|
|
54
|
+
compiledSchemas = {};
|
|
55
|
+
debug('Json-schema prep, using ajv')
|
|
56
|
+
return schemas;
|
|
41
57
|
}
|
package/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import {agg} from './aggregation/index.mjs';
|
|
|
2
2
|
import {crudlify as crud} from './crudlify/index.mjs';
|
|
3
3
|
import {serveStatic as ws, render as renderView, internalFetch} from './webserver.mjs';
|
|
4
4
|
import Workflow from './workflow/engine.mjs';
|
|
5
|
+
import { generateOpenApiSpec } from './openapi/generator.mjs';
|
|
6
|
+
import { generateSwaggerHtml } from './openapi/swagger-ui.mjs';
|
|
5
7
|
|
|
6
8
|
function createRoute(str) {
|
|
7
9
|
if(str instanceof RegExp) {
|
|
@@ -22,29 +24,54 @@ class Codehooks {
|
|
|
22
24
|
workers = {};
|
|
23
25
|
realtime = {};
|
|
24
26
|
startup = {};
|
|
25
|
-
|
|
27
|
+
openApiMeta = {}; // Store route documentation metadata
|
|
28
|
+
|
|
29
|
+
// Extract OpenAPI metadata from route handlers
|
|
30
|
+
_extractOpenApiMeta = (routeKey, hooks) => {
|
|
31
|
+
for (const hook of hooks) {
|
|
32
|
+
if (hook && hook._openApiSpec) {
|
|
33
|
+
this.openApiMeta[routeKey] = {
|
|
34
|
+
...this.openApiMeta[routeKey],
|
|
35
|
+
...hook._openApiSpec
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
26
41
|
post = (path, ...hook) => {
|
|
27
|
-
|
|
42
|
+
const routeKey = `POST ${createRoute(path)}`;
|
|
43
|
+
this.routes[routeKey] = hook;
|
|
44
|
+
this._extractOpenApiMeta(routeKey, hook);
|
|
28
45
|
};
|
|
29
46
|
|
|
30
47
|
get = (path, ...hook) => {
|
|
31
|
-
|
|
48
|
+
const routeKey = `GET ${createRoute(path)}`;
|
|
49
|
+
this.routes[routeKey] = hook;
|
|
50
|
+
this._extractOpenApiMeta(routeKey, hook);
|
|
32
51
|
};
|
|
33
52
|
|
|
34
53
|
put = (path, ...hook) => {
|
|
35
|
-
|
|
54
|
+
const routeKey = `PUT ${createRoute(path)}`;
|
|
55
|
+
this.routes[routeKey] = hook;
|
|
56
|
+
this._extractOpenApiMeta(routeKey, hook);
|
|
36
57
|
};
|
|
37
58
|
|
|
38
59
|
patch = (path, ...hook) => {
|
|
39
|
-
|
|
60
|
+
const routeKey = `PATCH ${createRoute(path)}`;
|
|
61
|
+
this.routes[routeKey] = hook;
|
|
62
|
+
this._extractOpenApiMeta(routeKey, hook);
|
|
40
63
|
};
|
|
41
64
|
|
|
42
65
|
delete = (path, ...hook) => {
|
|
43
|
-
|
|
66
|
+
const routeKey = `DELETE ${createRoute(path)}`;
|
|
67
|
+
this.routes[routeKey] = hook;
|
|
68
|
+
this._extractOpenApiMeta(routeKey, hook);
|
|
44
69
|
};
|
|
45
70
|
|
|
46
71
|
all = (path, ...hook) => {
|
|
47
|
-
|
|
72
|
+
const routeKey = `* ${createRoute(path)}`;
|
|
73
|
+
this.routes[routeKey] = hook;
|
|
74
|
+
this._extractOpenApiMeta(routeKey, hook);
|
|
48
75
|
};
|
|
49
76
|
|
|
50
77
|
auth = (path, ...hook) => {
|
|
@@ -110,6 +137,46 @@ class Codehooks {
|
|
|
110
137
|
return crud(Codehooks.getInstance(), schema, options);
|
|
111
138
|
}
|
|
112
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Enable OpenAPI documentation
|
|
142
|
+
* @param {object} config - OpenAPI configuration
|
|
143
|
+
* @param {string} [uiPath='/docs'] - Path to serve Swagger UI
|
|
144
|
+
* @returns {Codehooks} this for chaining
|
|
145
|
+
*/
|
|
146
|
+
openapi = (config = {}, uiPath = '/docs') => {
|
|
147
|
+
const specPath = config.specPath || '/openapi.json';
|
|
148
|
+
|
|
149
|
+
// Store OpenAPI config
|
|
150
|
+
this.set('_openapi_config', config);
|
|
151
|
+
|
|
152
|
+
// Route: Serve OpenAPI JSON spec
|
|
153
|
+
this.get(specPath, async (req, res) => {
|
|
154
|
+
const spec = await generateOpenApiSpec(this, config);
|
|
155
|
+
res.set('Content-Type', 'application/json');
|
|
156
|
+
res.json(spec);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Route: Serve Swagger UI HTML
|
|
160
|
+
this.get(uiPath, async (req, res) => {
|
|
161
|
+
// Build spec URL relative to current path
|
|
162
|
+
const specUrl = specPath;
|
|
163
|
+
|
|
164
|
+
const html = generateSwaggerHtml(specUrl, {
|
|
165
|
+
title: config.info?.title || 'API Documentation',
|
|
166
|
+
...config.swaggerUi
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
res.set('Content-Type', 'text/html');
|
|
170
|
+
res.send(html);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Bypass auth for doc routes
|
|
174
|
+
this.auth(specPath, (req, res, next) => next());
|
|
175
|
+
this.auth(uiPath, (req, res, next) => next());
|
|
176
|
+
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
|
|
113
180
|
realtime = (path, ...hook) => {
|
|
114
181
|
this.realtime[path] = hook;
|
|
115
182
|
if (hook) {
|
|
@@ -206,6 +273,8 @@ export const app = _coho;
|
|
|
206
273
|
export {
|
|
207
274
|
Workflow
|
|
208
275
|
};
|
|
276
|
+
// OpenAPI documentation helpers
|
|
277
|
+
export { openapi, response, body, param, query, header } from './openapi/index.mjs';
|
|
209
278
|
export const realtime = {
|
|
210
279
|
createChannel: (path, ...hook) => {
|
|
211
280
|
Codehooks.getInstance().realtime(path, ...hook);
|