json-rest-api 1.0.0 → 1.0.2
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/.claude/settings.local.json +16 -0
- package/LICENSE +674 -0
- package/README.md +655 -309
- package/RELATIONSHIPS.md +1013 -0
- package/TODO_rest-api-plugin-extras.js +142 -0
- package/connectors_ideas/express-plugin.js +120 -0
- package/connectors_ideas/fastify.js +219 -0
- package/connectors_ideas/http-plugin.js +138 -0
- package/connectors_ideas/koa-plugin.js +113 -0
- package/docs/fileUploads.md +722 -0
- package/docs/mini-guides/file-uploads-complete.md +159 -0
- package/examples/.gitkeep +0 -0
- package/examples/express-example.js +262 -0
- package/examples/express-plugin.js +366 -0
- package/examples/file-upload-example.js +205 -0
- package/examples/filename-strategies.js +140 -0
- package/examples/knex-basic.js +66 -0
- package/index.js +31 -56
- package/lib/payload-validators.js +685 -0
- package/lib/rest-api-errors.js +58 -0
- package/package.json +41 -14
- package/payloadValdators.js +686 -0
- package/plugins/core/connectors/express-plugin.js +449 -0
- package/plugins/core/connectors/http-plugin.js +450 -0
- package/plugins/core/file-handling-plugin.js +338 -0
- package/plugins/core/lib/busboy-detector.js +167 -0
- package/plugins/core/lib/formidable-detector.js +130 -0
- package/plugins/core/rest-api-knex-plugin.js +200 -0
- package/plugins/core/rest-api-plugin.js +1419 -0
- package/plugins/storage/local-storage.js +218 -0
- package/plugins/storage/s3-storage.js +125 -0
- package/test-examples/package-lock.json +906 -0
- package/test-examples/package.json +15 -0
- package/test-examples/test-basic-api.js +11 -0
- package/test-examples/test-books-resource.js +116 -0
- package/test-examples/test-complete-file-upload.js +140 -0
- package/test-examples/test-debug.js +28 -0
- package/test-examples/test-express-plugin.js +177 -0
- package/test-examples/test-express-simple.js +28 -0
- package/test-examples/test-express-with-import.js +71 -0
- package/test-examples/test-file-upload.js +132 -0
- package/test-examples/test-http-plugin.js +159 -0
- package/test-examples/test-programmatic-api.js +188 -0
- package/test-examples/test-rest-api-plugin.js +13 -0
- package/TODO.todo +0 -113
- package/api.js +0 -628
- package/docs/API.md +0 -1306
- package/docs/FIXES_APPLIED.md +0 -59
- package/docs/GUIDES.md +0 -1792
- package/docs/RESOURCES_IN_EXPRESS.md +0 -456
- package/docs/example-versioning.js +0 -168
- package/docs/example.js +0 -195
- package/plugins/API guide.md +0 -364
- package/plugins/api-registry.js +0 -142
- package/plugins/api-versioning.md +0 -135
- package/plugins/http.js +0 -273
- package/plugins/logging.js +0 -255
- package/plugins/memory.js +0 -182
- package/plugins/mysql.js +0 -753
- package/plugins/openapi.js +0 -401
- package/plugins/positioning.js +0 -220
- package/plugins/security.js +0 -261
- package/plugins/validation.js +0 -173
- package/plugins/versioning.js +0 -288
- package/resource-helper.js +0 -79
- package/schema.js +0 -481
- package/test-basic.js +0 -107
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
1. Creating Versioned APIs
|
|
3
|
-
|
|
4
|
-
// Just specify name and version - that's it!
|
|
5
|
-
const userApi = createApi({
|
|
6
|
-
name: 'users',
|
|
7
|
-
version: '1.0.0',
|
|
8
|
-
storage: 'memory'
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
// Create a new version
|
|
12
|
-
const userApiV2 = createApi({
|
|
13
|
-
name: 'users',
|
|
14
|
-
version: '2.0.0',
|
|
15
|
-
storage: 'memory'
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
2. Using APIs Programmatically
|
|
19
|
-
|
|
20
|
-
// Get latest version automatically
|
|
21
|
-
const api = Api.get('users', 'latest');
|
|
22
|
-
|
|
23
|
-
// Get specific version
|
|
24
|
-
const apiV1 = Api.get('users', '1.0.0');
|
|
25
|
-
|
|
26
|
-
// Get minimum version (2.0.0 or higher)
|
|
27
|
-
const apiV2Plus = Api.get('users', '2.0.0');
|
|
28
|
-
|
|
29
|
-
// Use it
|
|
30
|
-
const user = await api.insert({
|
|
31
|
-
name: 'John',
|
|
32
|
-
email: 'john@example.com'
|
|
33
|
-
}, { type: 'users' });
|
|
34
|
-
|
|
35
|
-
3. Cross-API Communication
|
|
36
|
-
|
|
37
|
-
APIs can access each other automatically with version compatibility:
|
|
38
|
-
|
|
39
|
-
const ordersApi = createApi({
|
|
40
|
-
name: 'orders',
|
|
41
|
-
version: '1.0.0'
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Inside orders API, access users API automatically
|
|
45
|
-
ordersApi.hook('afterInsert', async (context) => {
|
|
46
|
-
// This gets a compatible users API automatically!
|
|
47
|
-
const usersApi = ordersApi.apis.users;
|
|
48
|
-
const user = await usersApi.get(context.data.userId, { type: 'users'
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
4. HTTP Version Negotiation
|
|
53
|
-
|
|
54
|
-
The library handles all HTTP version routing automatically:
|
|
55
|
-
|
|
56
|
-
// Mount APIs - versioning is automatic
|
|
57
|
-
userApiV1.mount(app); // Available at /api/1.0.0/users
|
|
58
|
-
userApiV2.mount(app); // Available at /api/2.0.0/users
|
|
59
|
-
|
|
60
|
-
// Clients can request versions:
|
|
61
|
-
// Via header
|
|
62
|
-
fetch('/api/users', {
|
|
63
|
-
headers: { 'API-Version': '2.0.0' }
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Via query
|
|
67
|
-
fetch('/api/users?v=2.0.0');
|
|
68
|
-
|
|
69
|
-
// Via path
|
|
70
|
-
fetch('/api/2.0.0/users');
|
|
71
|
-
|
|
72
|
-
5. Version Resolution Rules
|
|
73
|
-
|
|
74
|
-
The library automatically finds the right version:
|
|
75
|
-
|
|
76
|
-
- 'latest' → Newest version
|
|
77
|
-
- '2.0.0' → Exactly 2.0.0 OR the newest version ≥ 2.0.0
|
|
78
|
-
- '^2.0.0' → Any 2.x.x version (npm style)
|
|
79
|
-
- '~2.1.0' → Any 2.1.x version (npm style)
|
|
80
|
-
- '>=2.0.0' → Any version ≥ 2.0.0
|
|
81
|
-
|
|
82
|
-
6. Complete Working Example
|
|
83
|
-
|
|
84
|
-
// Define v1
|
|
85
|
-
const apiV1 = createApi({
|
|
86
|
-
name: 'products',
|
|
87
|
-
version: '1.0.0',
|
|
88
|
-
storage: 'memory'
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
apiV1.addResource('products', new Schema({
|
|
92
|
-
id: { type: 'id' },
|
|
93
|
-
name: { type: 'string', required: true },
|
|
94
|
-
price: { type: 'number', required: true }
|
|
95
|
-
}));
|
|
96
|
-
|
|
97
|
-
// Define v2 with new field
|
|
98
|
-
const apiV2 = createApi({
|
|
99
|
-
name: 'products',
|
|
100
|
-
version: '2.0.0',
|
|
101
|
-
storage: 'memory'
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
apiV2.addResource('products', new Schema({
|
|
105
|
-
id: { type: 'id' },
|
|
106
|
-
name: { type: 'string', required: true },
|
|
107
|
-
price: { type: 'number', required: true },
|
|
108
|
-
category: { type: 'string', required: true } // New!
|
|
109
|
-
}));
|
|
110
|
-
|
|
111
|
-
// Use programmatically - version selection is automatic
|
|
112
|
-
async function createProduct(data) {
|
|
113
|
-
// Automatically picks the right version based on data
|
|
114
|
-
const hasCategory = 'category' in data;
|
|
115
|
-
const minVersion = hasCategory ? '2.0.0' : '1.0.0';
|
|
116
|
-
|
|
117
|
-
const api = Api.get('products', minVersion);
|
|
118
|
-
return api.insert(data, { type: 'products' });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Works with v1
|
|
122
|
-
await createProduct({
|
|
123
|
-
name: 'Widget',
|
|
124
|
-
price: 9.99
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// Automatically uses v2
|
|
128
|
-
await createProduct({
|
|
129
|
-
name: 'Gadget',
|
|
130
|
-
price: 19.99,
|
|
131
|
-
category: 'Electronics'
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
The library handles all the complexity - you just specify name and
|
|
135
|
-
version when creating APIs, and the library does the rest!
|
package/plugins/http.js
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* HTTP plugin for JSON REST API with JSON:API compliance
|
|
5
|
-
*/
|
|
6
|
-
export const HTTPPlugin = {
|
|
7
|
-
install(api, options = {}) {
|
|
8
|
-
const router = express.Router();
|
|
9
|
-
api.router = router;
|
|
10
|
-
|
|
11
|
-
// Middleware for JSON parsing
|
|
12
|
-
router.use(express.json({
|
|
13
|
-
type: ['application/json', 'application/vnd.api+json']
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
// Base path for all routes
|
|
17
|
-
const basePath = options.basePath || '/api';
|
|
18
|
-
|
|
19
|
-
// Helper to parse JSON:API request body
|
|
20
|
-
const parseJsonApiBody = (body) => {
|
|
21
|
-
if (body.data) {
|
|
22
|
-
if (body.data.attributes) {
|
|
23
|
-
// JSON:API format
|
|
24
|
-
return { ...body.data.attributes, id: body.data.id };
|
|
25
|
-
}
|
|
26
|
-
// Assume data is the actual data
|
|
27
|
-
return body.data;
|
|
28
|
-
}
|
|
29
|
-
// Plain format
|
|
30
|
-
return body;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Helper to format JSON:API errors
|
|
34
|
-
const formatErrors = (errors, status = 400) => {
|
|
35
|
-
if (!Array.isArray(errors)) {
|
|
36
|
-
errors = [errors];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
errors: errors.map(err => {
|
|
41
|
-
if (err.status) return err; // Already formatted
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
status: String(status),
|
|
45
|
-
title: err.code || 'Error',
|
|
46
|
-
detail: err.message || String(err),
|
|
47
|
-
source: err.field ? { pointer: `/data/attributes/${err.field}` } : undefined
|
|
48
|
-
};
|
|
49
|
-
})
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Parse query parameters
|
|
54
|
-
const parseQueryParams = (req) => {
|
|
55
|
-
const params = {
|
|
56
|
-
filter: {},
|
|
57
|
-
sort: req.query.sort,
|
|
58
|
-
page: {
|
|
59
|
-
size: req.query['page[size]'] || req.query.pageSize,
|
|
60
|
-
number: req.query['page[number]'] || req.query.page
|
|
61
|
-
},
|
|
62
|
-
include: req.query.include,
|
|
63
|
-
fields: {}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Parse filters
|
|
67
|
-
for (const [key, value] of Object.entries(req.query)) {
|
|
68
|
-
if (key.startsWith('filter[') && key.endsWith(']')) {
|
|
69
|
-
const filterKey = key.slice(7, -1);
|
|
70
|
-
params.filter[filterKey] = value;
|
|
71
|
-
} else if (key.startsWith('fields[') && key.endsWith(']')) {
|
|
72
|
-
const fieldType = key.slice(7, -1);
|
|
73
|
-
params.fields[fieldType] = value.split(',');
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Legacy filter support
|
|
78
|
-
if (Object.keys(params.filter).length === 0) {
|
|
79
|
-
// Copy non-standard query params as filters
|
|
80
|
-
for (const [key, value] of Object.entries(req.query)) {
|
|
81
|
-
if (!['sort', 'page', 'pageSize', 'include', 'fields'].includes(key) &&
|
|
82
|
-
!key.includes('[')) {
|
|
83
|
-
params.filter[key] = value;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return params;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// Build links for pagination
|
|
92
|
-
const buildLinks = (req, params, meta) => {
|
|
93
|
-
const links = {};
|
|
94
|
-
const baseUrl = `${req.protocol}://${req.get('host')}${req.baseUrl}${req.path}`;
|
|
95
|
-
const queryParams = new URLSearchParams(req.query);
|
|
96
|
-
|
|
97
|
-
// Self link
|
|
98
|
-
links.self = `${baseUrl}?${queryParams}`;
|
|
99
|
-
|
|
100
|
-
if (meta.totalPages > 1) {
|
|
101
|
-
const currentPage = meta.pageNumber;
|
|
102
|
-
|
|
103
|
-
// First page
|
|
104
|
-
queryParams.set('page[number]', '1');
|
|
105
|
-
links.first = `${baseUrl}?${queryParams}`;
|
|
106
|
-
|
|
107
|
-
// Last page
|
|
108
|
-
queryParams.set('page[number]', String(meta.totalPages));
|
|
109
|
-
links.last = `${baseUrl}?${queryParams}`;
|
|
110
|
-
|
|
111
|
-
// Previous page
|
|
112
|
-
if (currentPage > 1) {
|
|
113
|
-
queryParams.set('page[number]', String(currentPage - 1));
|
|
114
|
-
links.prev = `${baseUrl}?${queryParams}`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Next page
|
|
118
|
-
if (currentPage < meta.totalPages) {
|
|
119
|
-
queryParams.set('page[number]', String(currentPage + 1));
|
|
120
|
-
links.next = `${baseUrl}?${queryParams}`;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return links;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// Version-aware routing
|
|
128
|
-
if (api.options.name && api.options.version) {
|
|
129
|
-
// Add version info to all responses
|
|
130
|
-
router.use((req, res, next) => {
|
|
131
|
-
res.set('API-Version', api.options.version);
|
|
132
|
-
next();
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Routes
|
|
137
|
-
|
|
138
|
-
// GET collection
|
|
139
|
-
router.get('/:type', async (req, res) => {
|
|
140
|
-
try {
|
|
141
|
-
const params = parseQueryParams(req);
|
|
142
|
-
const result = await api.query(params, {
|
|
143
|
-
type: req.params.type,
|
|
144
|
-
...options.typeOptions?.[req.params.type]
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Add links
|
|
148
|
-
if (result.meta) {
|
|
149
|
-
result.links = buildLinks(req, params, result.meta);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
res.json(result);
|
|
153
|
-
} catch (error) {
|
|
154
|
-
const status = error.status || 400;
|
|
155
|
-
res.status(status).json(formatErrors(error.errors || error, status));
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// GET single resource
|
|
160
|
-
router.get('/:type/:id', async (req, res) => {
|
|
161
|
-
try {
|
|
162
|
-
const result = await api.get(req.params.id, {
|
|
163
|
-
type: req.params.type,
|
|
164
|
-
...options.typeOptions?.[req.params.type]
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
if (!result.data) {
|
|
168
|
-
res.status(404).json(formatErrors({
|
|
169
|
-
message: 'Resource not found',
|
|
170
|
-
code: 'NOT_FOUND'
|
|
171
|
-
}, 404));
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
res.json(result);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
const status = error.status || 400;
|
|
178
|
-
res.status(status).json(formatErrors(error.errors || error, status));
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// POST new resource
|
|
183
|
-
router.post('/:type', async (req, res) => {
|
|
184
|
-
try {
|
|
185
|
-
const data = parseJsonApiBody(req.body);
|
|
186
|
-
const result = await api.insert(data, {
|
|
187
|
-
type: req.params.type,
|
|
188
|
-
...options.typeOptions?.[req.params.type]
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
res.status(201)
|
|
192
|
-
.location(`${basePath}/${req.params.type}/${result.data.id}`)
|
|
193
|
-
.json(result);
|
|
194
|
-
} catch (error) {
|
|
195
|
-
const status = error.status || 400;
|
|
196
|
-
res.status(status).json(formatErrors(error.errors || error, status));
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// PATCH update resource
|
|
201
|
-
router.patch('/:type/:id', async (req, res) => {
|
|
202
|
-
try {
|
|
203
|
-
const data = parseJsonApiBody(req.body);
|
|
204
|
-
const result = await api.update(req.params.id, data, {
|
|
205
|
-
type: req.params.type,
|
|
206
|
-
...options.typeOptions?.[req.params.type]
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
if (!result.data) {
|
|
210
|
-
res.status(404).json(formatErrors({
|
|
211
|
-
message: 'Resource not found',
|
|
212
|
-
code: 'NOT_FOUND'
|
|
213
|
-
}, 404));
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
res.json(result);
|
|
218
|
-
} catch (error) {
|
|
219
|
-
const status = error.status || 400;
|
|
220
|
-
res.status(status).json(formatErrors(error.errors || error, status));
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// DELETE resource
|
|
225
|
-
router.delete('/:type/:id', async (req, res) => {
|
|
226
|
-
try {
|
|
227
|
-
await api.delete(req.params.id, {
|
|
228
|
-
type: req.params.type,
|
|
229
|
-
...options.typeOptions?.[req.params.type]
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
res.status(204).end();
|
|
233
|
-
} catch (error) {
|
|
234
|
-
const status = error.status || 400;
|
|
235
|
-
res.status(status).json(formatErrors(error.errors || error, status));
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// OPTIONS for CORS
|
|
240
|
-
router.options('*', (req, res) => {
|
|
241
|
-
res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
|
|
242
|
-
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
243
|
-
res.status(204).end();
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// Mount router on Express app if provided
|
|
247
|
-
if (options.app) {
|
|
248
|
-
options.app.use(basePath, router);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Add method to manually mount router
|
|
252
|
-
api.mount = (app, path = basePath) => {
|
|
253
|
-
app.use(path, router);
|
|
254
|
-
return api;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
// Add middleware support
|
|
258
|
-
api.useMiddleware = (middleware) => {
|
|
259
|
-
router.use(middleware);
|
|
260
|
-
return api;
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// Add route-specific middleware
|
|
264
|
-
api.useRouteMiddleware = (method, path, ...middlewares) => {
|
|
265
|
-
// Store middleware to be applied when routes are defined
|
|
266
|
-
if (!api._routeMiddlewares) {
|
|
267
|
-
api._routeMiddlewares = [];
|
|
268
|
-
}
|
|
269
|
-
api._routeMiddlewares.push({ method, path, middlewares });
|
|
270
|
-
return api;
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
};
|
package/plugins/logging.js
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Logging plugin implementing structured logging best practices
|
|
3
|
-
*/
|
|
4
|
-
export const LoggingPlugin = {
|
|
5
|
-
install(api, options = {}) {
|
|
6
|
-
const defaultOptions = {
|
|
7
|
-
level: process.env.LOG_LEVEL || 'info',
|
|
8
|
-
format: 'json', // 'json' or 'pretty'
|
|
9
|
-
includeRequest: true,
|
|
10
|
-
includeResponse: true,
|
|
11
|
-
includeTiming: true,
|
|
12
|
-
sensitiveFields: ['password', 'token', 'secret', 'authorization'],
|
|
13
|
-
logger: console, // Can be replaced with winston, bunyan, etc.
|
|
14
|
-
...options
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const levels = {
|
|
18
|
-
error: 0,
|
|
19
|
-
warn: 1,
|
|
20
|
-
info: 2,
|
|
21
|
-
debug: 3
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// Create structured logger
|
|
25
|
-
const log = (level, message, meta = {}) => {
|
|
26
|
-
if (levels[level] > levels[defaultOptions.level]) return;
|
|
27
|
-
|
|
28
|
-
const timestamp = new Date().toISOString();
|
|
29
|
-
const logEntry = {
|
|
30
|
-
timestamp,
|
|
31
|
-
level,
|
|
32
|
-
message,
|
|
33
|
-
...meta
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Remove sensitive data
|
|
37
|
-
const sanitized = sanitizeLogData(logEntry, defaultOptions.sensitiveFields);
|
|
38
|
-
|
|
39
|
-
if (defaultOptions.format === 'json') {
|
|
40
|
-
defaultOptions.logger[level](JSON.stringify(sanitized));
|
|
41
|
-
} else {
|
|
42
|
-
defaultOptions.logger[level](`[${timestamp}] ${level.toUpperCase()}: ${message}`, meta);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Add logging methods to API
|
|
47
|
-
api.log = {
|
|
48
|
-
error: (message, meta) => log('error', message, meta),
|
|
49
|
-
warn: (message, meta) => log('warn', message, meta),
|
|
50
|
-
info: (message, meta) => log('info', message, meta),
|
|
51
|
-
debug: (message, meta) => log('debug', message, meta)
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Log all operations
|
|
55
|
-
const timings = new WeakMap();
|
|
56
|
-
|
|
57
|
-
// Start timing
|
|
58
|
-
api.hook('beforeValidate', async (context) => {
|
|
59
|
-
if (defaultOptions.includeTiming) {
|
|
60
|
-
timings.set(context, Date.now());
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (defaultOptions.includeRequest) {
|
|
64
|
-
api.log.info(`${context.method.toUpperCase()} ${context.options.type}`, {
|
|
65
|
-
operation: context.method,
|
|
66
|
-
type: context.options.type,
|
|
67
|
-
id: context.id,
|
|
68
|
-
requestId: context.options.requestId,
|
|
69
|
-
userId: context.options.userId,
|
|
70
|
-
data: context.method === 'query' ? context.params : context.data
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Log successful operations
|
|
76
|
-
api.hook('beforeSend', async (context) => {
|
|
77
|
-
const duration = defaultOptions.includeTiming && timings.has(context)
|
|
78
|
-
? Date.now() - timings.get(context)
|
|
79
|
-
: undefined;
|
|
80
|
-
|
|
81
|
-
if (defaultOptions.includeResponse) {
|
|
82
|
-
api.log.info(`${context.method.toUpperCase()} ${context.options.type} completed`, {
|
|
83
|
-
operation: context.method,
|
|
84
|
-
type: context.options.type,
|
|
85
|
-
id: context.id || context.result?.id,
|
|
86
|
-
requestId: context.options.requestId,
|
|
87
|
-
userId: context.options.userId,
|
|
88
|
-
duration,
|
|
89
|
-
resultCount: context.results?.length
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Clean up timing
|
|
94
|
-
if (timings.has(context)) {
|
|
95
|
-
timings.delete(context);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Log errors
|
|
100
|
-
api.hook('afterValidate', async (context) => {
|
|
101
|
-
if (context.errors.length > 0) {
|
|
102
|
-
api.log.warn('Validation errors', {
|
|
103
|
-
operation: context.method,
|
|
104
|
-
type: context.options.type,
|
|
105
|
-
errors: context.errors,
|
|
106
|
-
requestId: context.options.requestId,
|
|
107
|
-
userId: context.options.userId
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Log database operations
|
|
113
|
-
if (api.mysqlPools) {
|
|
114
|
-
// Wrap MySQL query method to add logging
|
|
115
|
-
const originalGetConnection = api.getConnection;
|
|
116
|
-
api.getConnection = (name) => {
|
|
117
|
-
const conn = originalGetConnection.call(api, name);
|
|
118
|
-
|
|
119
|
-
// Wrap the query method
|
|
120
|
-
const originalQuery = conn.pool.query.bind(conn.pool);
|
|
121
|
-
conn.pool.query = async (sql, params) => {
|
|
122
|
-
const start = Date.now();
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const result = await originalQuery(sql, params);
|
|
126
|
-
|
|
127
|
-
api.log.debug('SQL query executed', {
|
|
128
|
-
sql: sql.substring(0, 200), // Truncate long queries
|
|
129
|
-
duration: Date.now() - start,
|
|
130
|
-
rowCount: result[0]?.length
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
return result;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
api.log.error('SQL query failed', {
|
|
136
|
-
sql: sql.substring(0, 200),
|
|
137
|
-
error: error.message,
|
|
138
|
-
duration: Date.now() - start
|
|
139
|
-
});
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
return conn;
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// HTTP request logging
|
|
149
|
-
if (api.router) {
|
|
150
|
-
api.router.use((req, res, next) => {
|
|
151
|
-
const start = Date.now();
|
|
152
|
-
const requestId = req.headers['x-request-id'] || generateId();
|
|
153
|
-
|
|
154
|
-
// Add request ID to context
|
|
155
|
-
req.requestId = requestId;
|
|
156
|
-
|
|
157
|
-
// Log request
|
|
158
|
-
api.log.info('HTTP request', {
|
|
159
|
-
method: req.method,
|
|
160
|
-
url: req.url,
|
|
161
|
-
ip: req.ip,
|
|
162
|
-
userAgent: req.headers['user-agent'],
|
|
163
|
-
requestId
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Log response
|
|
167
|
-
const originalSend = res.send;
|
|
168
|
-
res.send = function(data) {
|
|
169
|
-
res.send = originalSend;
|
|
170
|
-
|
|
171
|
-
api.log.info('HTTP response', {
|
|
172
|
-
method: req.method,
|
|
173
|
-
url: req.url,
|
|
174
|
-
status: res.statusCode,
|
|
175
|
-
duration: Date.now() - start,
|
|
176
|
-
requestId
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
return res.send(data);
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
next();
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Audit logging for sensitive operations
|
|
187
|
-
api.hook('afterInsert', async (context) => {
|
|
188
|
-
if (options.auditLog) {
|
|
189
|
-
api.log.info('Resource created', {
|
|
190
|
-
audit: true,
|
|
191
|
-
operation: 'create',
|
|
192
|
-
type: context.options.type,
|
|
193
|
-
id: context.result?.id,
|
|
194
|
-
userId: context.options.userId,
|
|
195
|
-
changes: context.data
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
api.hook('afterUpdate', async (context) => {
|
|
201
|
-
if (options.auditLog) {
|
|
202
|
-
api.log.info('Resource updated', {
|
|
203
|
-
audit: true,
|
|
204
|
-
operation: 'update',
|
|
205
|
-
type: context.options.type,
|
|
206
|
-
id: context.id,
|
|
207
|
-
userId: context.options.userId,
|
|
208
|
-
changes: context.data
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
api.hook('afterDelete', async (context) => {
|
|
214
|
-
if (options.auditLog) {
|
|
215
|
-
api.log.info('Resource deleted', {
|
|
216
|
-
audit: true,
|
|
217
|
-
operation: 'delete',
|
|
218
|
-
type: context.options.type,
|
|
219
|
-
id: context.id,
|
|
220
|
-
userId: context.options.userId
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
// Helper to sanitize sensitive data
|
|
228
|
-
function sanitizeLogData(data, sensitiveFields) {
|
|
229
|
-
if (typeof data !== 'object' || data === null) return data;
|
|
230
|
-
|
|
231
|
-
const sanitized = Array.isArray(data) ? [] : {};
|
|
232
|
-
|
|
233
|
-
for (const [key, value] of Object.entries(data)) {
|
|
234
|
-
const lowerKey = key.toLowerCase();
|
|
235
|
-
|
|
236
|
-
// Check if field is sensitive
|
|
237
|
-
const isSensitive = sensitiveFields.some(field =>
|
|
238
|
-
lowerKey.includes(field.toLowerCase())
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
if (isSensitive) {
|
|
242
|
-
sanitized[key] = '[REDACTED]';
|
|
243
|
-
} else if (typeof value === 'object') {
|
|
244
|
-
sanitized[key] = sanitizeLogData(value, sensitiveFields);
|
|
245
|
-
} else {
|
|
246
|
-
sanitized[key] = value;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return sanitized;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function generateId() {
|
|
254
|
-
return Math.random().toString(36).substring(2, 15);
|
|
255
|
-
}
|