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 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
 
@@ -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
- //z-schema method
2
- let validator = null;
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
- * @returns
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
- const valid = options.validator.validate(document, schema);
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(options.validator.getLastErrors());
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
- validator = options.validator;
39
- debug('Json-schema prep', options)
40
- return schemas;
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
- this.routes[`POST ${createRoute(path)}`] = hook;
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
- this.routes[`GET ${createRoute(path)}`] = hook;
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
- this.routes[`PUT ${createRoute(path)}`] = hook;
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
- this.routes[`PATCH ${createRoute(path)}`] = hook;
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
- this.routes[`DELETE ${createRoute(path)}`] = hook;
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
- this.routes[`* ${createRoute(path)}`] = hook;
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);