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
package/plugins/versioning.js
DELETED
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Versioning plugin for API versioning and resource version management
|
|
3
|
-
*/
|
|
4
|
-
export const VersioningPlugin = {
|
|
5
|
-
install(api, options = {}) {
|
|
6
|
-
const defaultOptions = {
|
|
7
|
-
// API versioning
|
|
8
|
-
apiVersion: '1.0.0',
|
|
9
|
-
versionHeader: 'api-version',
|
|
10
|
-
versionParam: 'v',
|
|
11
|
-
|
|
12
|
-
// Resource versioning
|
|
13
|
-
versionField: 'version',
|
|
14
|
-
lastModifiedField: 'lastModified',
|
|
15
|
-
modifiedByField: 'modifiedBy',
|
|
16
|
-
trackHistory: false,
|
|
17
|
-
historyTable: null,
|
|
18
|
-
|
|
19
|
-
...options
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// Store versioning options
|
|
23
|
-
api.versioningOptions = defaultOptions;
|
|
24
|
-
|
|
25
|
-
// API Version Management
|
|
26
|
-
if (defaultOptions.apiVersion) {
|
|
27
|
-
// Add version info to all responses
|
|
28
|
-
api.hook('beforeSend', async (context) => {
|
|
29
|
-
if (!context.meta) context.meta = {};
|
|
30
|
-
context.meta.apiVersion = defaultOptions.apiVersion;
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Version negotiation for HTTP
|
|
34
|
-
if (api.router) {
|
|
35
|
-
api.router.use((req, res, next) => {
|
|
36
|
-
// Check version header
|
|
37
|
-
const headerVersion = req.headers[defaultOptions.versionHeader];
|
|
38
|
-
// Check version query param
|
|
39
|
-
const paramVersion = req.query[defaultOptions.versionParam];
|
|
40
|
-
|
|
41
|
-
const requestedVersion = headerVersion || paramVersion;
|
|
42
|
-
|
|
43
|
-
if (requestedVersion && requestedVersion !== defaultOptions.apiVersion) {
|
|
44
|
-
// Handle version mismatch
|
|
45
|
-
if (defaultOptions.strict) {
|
|
46
|
-
return res.status(400).json({
|
|
47
|
-
errors: [{
|
|
48
|
-
status: '400',
|
|
49
|
-
title: 'Version Mismatch',
|
|
50
|
-
detail: `API version ${requestedVersion} not supported. Current version is ${defaultOptions.apiVersion}`
|
|
51
|
-
}]
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Add version to response headers
|
|
57
|
-
res.set('API-Version', defaultOptions.apiVersion);
|
|
58
|
-
next();
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Resource Version Management
|
|
64
|
-
if (defaultOptions.versionField) {
|
|
65
|
-
// Initialize version on insert
|
|
66
|
-
api.hook('beforeInsert', async (context) => {
|
|
67
|
-
const { data, options } = context;
|
|
68
|
-
if (options.versioning === false) return;
|
|
69
|
-
|
|
70
|
-
// Set initial version
|
|
71
|
-
if (data[defaultOptions.versionField] === undefined) {
|
|
72
|
-
data[defaultOptions.versionField] = 1;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Set modified timestamp
|
|
76
|
-
if (defaultOptions.lastModifiedField) {
|
|
77
|
-
data[defaultOptions.lastModifiedField] = new Date().toISOString();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Set modified by
|
|
81
|
-
if (defaultOptions.modifiedByField && options.userId) {
|
|
82
|
-
data[defaultOptions.modifiedByField] = options.userId;
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Increment version on update
|
|
87
|
-
api.hook('beforeUpdate', async (context) => {
|
|
88
|
-
const { data, options, id } = context;
|
|
89
|
-
if (options.versioning === false) return;
|
|
90
|
-
|
|
91
|
-
// Check for optimistic locking
|
|
92
|
-
if (defaultOptions.optimisticLocking && data[defaultOptions.versionField]) {
|
|
93
|
-
const currentVersion = data[defaultOptions.versionField];
|
|
94
|
-
delete data[defaultOptions.versionField]; // Remove from update data
|
|
95
|
-
|
|
96
|
-
// Get current record
|
|
97
|
-
const impl = api.implementers.get('get');
|
|
98
|
-
const current = await impl({ id, options });
|
|
99
|
-
|
|
100
|
-
if (current && current[defaultOptions.versionField] !== currentVersion) {
|
|
101
|
-
const error = new Error('Version conflict - record has been modified');
|
|
102
|
-
error.status = 409;
|
|
103
|
-
error.code = 'VERSION_CONFLICT';
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Save current version for history
|
|
109
|
-
if (defaultOptions.trackHistory) {
|
|
110
|
-
const impl = api.implementers.get('get');
|
|
111
|
-
context.previousVersion = await impl({ id, options });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Increment version
|
|
115
|
-
if (!options.skipVersioning) {
|
|
116
|
-
const impl = api.implementers.get('get');
|
|
117
|
-
const current = await impl({ id, options });
|
|
118
|
-
|
|
119
|
-
if (current) {
|
|
120
|
-
data[defaultOptions.versionField] = (current[defaultOptions.versionField] || 0) + 1;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Update modified timestamp
|
|
125
|
-
if (defaultOptions.lastModifiedField) {
|
|
126
|
-
data[defaultOptions.lastModifiedField] = new Date().toISOString();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Update modified by
|
|
130
|
-
if (defaultOptions.modifiedByField && options.userId) {
|
|
131
|
-
data[defaultOptions.modifiedByField] = options.userId;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Save version history after update
|
|
136
|
-
api.hook('afterUpdate', async (context) => {
|
|
137
|
-
if (!defaultOptions.trackHistory || !context.previousVersion) return;
|
|
138
|
-
|
|
139
|
-
await saveVersionHistory(api, context, defaultOptions);
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Version comparison helpers
|
|
144
|
-
api.compareVersions = (v1, v2) => {
|
|
145
|
-
if (typeof v1 === 'number' && typeof v2 === 'number') {
|
|
146
|
-
return v1 - v2;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Semantic version comparison
|
|
150
|
-
const parseVersion = (v) => {
|
|
151
|
-
const parts = String(v).split('.').map(Number);
|
|
152
|
-
return {
|
|
153
|
-
major: parts[0] || 0,
|
|
154
|
-
minor: parts[1] || 0,
|
|
155
|
-
patch: parts[2] || 0
|
|
156
|
-
};
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const ver1 = parseVersion(v1);
|
|
160
|
-
const ver2 = parseVersion(v2);
|
|
161
|
-
|
|
162
|
-
if (ver1.major !== ver2.major) return ver1.major - ver2.major;
|
|
163
|
-
if (ver1.minor !== ver2.minor) return ver1.minor - ver2.minor;
|
|
164
|
-
return ver1.patch - ver2.patch;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// Get version history
|
|
168
|
-
api.getVersionHistory = async (type, id, options = {}) => {
|
|
169
|
-
if (!defaultOptions.trackHistory) {
|
|
170
|
-
throw new Error('Version history tracking is not enabled');
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const historyTable = defaultOptions.historyTable || `${type}_history`;
|
|
174
|
-
|
|
175
|
-
// This would need to be implemented based on the storage plugin
|
|
176
|
-
const result = await api.query({
|
|
177
|
-
filter: {
|
|
178
|
-
resourceId: id,
|
|
179
|
-
resourceType: type
|
|
180
|
-
},
|
|
181
|
-
sort: `-${defaultOptions.versionField}`
|
|
182
|
-
}, {
|
|
183
|
-
type: historyTable,
|
|
184
|
-
...options
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
return result;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// Restore a specific version
|
|
191
|
-
api.restoreVersion = async (type, id, version, options = {}) => {
|
|
192
|
-
if (!defaultOptions.trackHistory) {
|
|
193
|
-
throw new Error('Version history tracking is not enabled');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Get the historical version
|
|
197
|
-
const history = await api.getVersionHistory(type, id, options);
|
|
198
|
-
const versionData = history.data.find(h =>
|
|
199
|
-
h.attributes[defaultOptions.versionField] === version
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
if (!versionData) {
|
|
203
|
-
throw new Error(`Version ${version} not found`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Restore the version
|
|
207
|
-
const { resourceId, resourceType, ...data } = versionData.attributes;
|
|
208
|
-
delete data.id; // Remove history record ID
|
|
209
|
-
|
|
210
|
-
return api.update(id, data, {
|
|
211
|
-
type,
|
|
212
|
-
...options,
|
|
213
|
-
skipVersioning: false // Ensure new version is created
|
|
214
|
-
});
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
// Version diffing
|
|
218
|
-
api.diffVersions = async (type, id, version1, version2, options = {}) => {
|
|
219
|
-
const history = await api.getVersionHistory(type, id, options);
|
|
220
|
-
|
|
221
|
-
const v1Data = history.data.find(h =>
|
|
222
|
-
h.attributes[defaultOptions.versionField] === version1
|
|
223
|
-
);
|
|
224
|
-
const v2Data = history.data.find(h =>
|
|
225
|
-
h.attributes[defaultOptions.versionField] === version2
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
if (!v1Data || !v2Data) {
|
|
229
|
-
throw new Error('One or both versions not found');
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Simple diff implementation
|
|
233
|
-
const diff = {
|
|
234
|
-
version1,
|
|
235
|
-
version2,
|
|
236
|
-
changes: []
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const v1Attrs = v1Data.attributes;
|
|
240
|
-
const v2Attrs = v2Data.attributes;
|
|
241
|
-
|
|
242
|
-
// Find changed fields
|
|
243
|
-
const allKeys = new Set([...Object.keys(v1Attrs), ...Object.keys(v2Attrs)]);
|
|
244
|
-
|
|
245
|
-
for (const key of allKeys) {
|
|
246
|
-
if (v1Attrs[key] !== v2Attrs[key]) {
|
|
247
|
-
diff.changes.push({
|
|
248
|
-
field: key,
|
|
249
|
-
oldValue: v1Attrs[key],
|
|
250
|
-
newValue: v2Attrs[key]
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return diff;
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Save version history
|
|
262
|
-
*/
|
|
263
|
-
async function saveVersionHistory(api, context, options) {
|
|
264
|
-
const { previousVersion, result, options: contextOptions } = context;
|
|
265
|
-
const historyTable = options.historyTable || `${contextOptions.type}_history`;
|
|
266
|
-
|
|
267
|
-
// Create history record
|
|
268
|
-
const historyData = {
|
|
269
|
-
...previousVersion,
|
|
270
|
-
resourceId: previousVersion[api.options.idProperty],
|
|
271
|
-
resourceType: contextOptions.type,
|
|
272
|
-
savedAt: new Date().toISOString()
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
// Remove the original ID to avoid conflicts
|
|
276
|
-
delete historyData[api.options.idProperty];
|
|
277
|
-
|
|
278
|
-
// Save to history table
|
|
279
|
-
try {
|
|
280
|
-
await api.insert(historyData, {
|
|
281
|
-
type: historyTable,
|
|
282
|
-
versioning: false // Don't version the history records
|
|
283
|
-
});
|
|
284
|
-
} catch (error) {
|
|
285
|
-
console.error('Failed to save version history:', error);
|
|
286
|
-
// Don't fail the main operation if history fails
|
|
287
|
-
}
|
|
288
|
-
}
|
package/resource-helper.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { Api, MySQLPlugin, ValidationPlugin, HTTPPlugin } from './index.js';
|
|
2
|
-
|
|
3
|
-
// Cache for API instances by version
|
|
4
|
-
const apiCache = new Map();
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Get or create a shared API instance for a version
|
|
8
|
-
*/
|
|
9
|
-
export function getVersionApi(version, config = {}) {
|
|
10
|
-
const cacheKey = `${config.name || 'app'}-${version}`;
|
|
11
|
-
|
|
12
|
-
if (apiCache.has(cacheKey)) {
|
|
13
|
-
return apiCache.get(cacheKey);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Create new API instance for this version
|
|
17
|
-
const api = new Api({
|
|
18
|
-
name: config.name || 'app',
|
|
19
|
-
version,
|
|
20
|
-
...config.apiOptions
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Add default plugins if not already added
|
|
24
|
-
if (!api.hasPlugin(ValidationPlugin)) {
|
|
25
|
-
api.use(ValidationPlugin);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (config.mysql && !api.hasPlugin(MySQLPlugin)) {
|
|
29
|
-
api.use(MySQLPlugin, config.mysql);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (config.http && !api.hasPlugin(HTTPPlugin)) {
|
|
33
|
-
api.use(HTTPPlugin, {
|
|
34
|
-
basePath: `/api/${version}`,
|
|
35
|
-
...config.http
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Add any additional plugins
|
|
40
|
-
if (config.plugins) {
|
|
41
|
-
for (const [plugin, options] of config.plugins) {
|
|
42
|
-
if (!api.hasPlugin(plugin)) {
|
|
43
|
-
api.use(plugin, options);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
apiCache.set(cacheKey, api);
|
|
49
|
-
return api;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Define a resource with automatic API management
|
|
54
|
-
*/
|
|
55
|
-
export function defineResource(version, name, definition) {
|
|
56
|
-
const api = getVersionApi(version, definition.api || {});
|
|
57
|
-
|
|
58
|
-
// Add the resource
|
|
59
|
-
api.addResource(name, definition.schema, definition.hooks);
|
|
60
|
-
|
|
61
|
-
// Configure storage options if needed
|
|
62
|
-
if (definition.storage) {
|
|
63
|
-
// Store metadata for storage plugins to use
|
|
64
|
-
api.resourceOptions = api.resourceOptions || new Map();
|
|
65
|
-
api.resourceOptions.set(name, definition.storage);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Sync schema in development
|
|
69
|
-
if (process.env.NODE_ENV === 'development' && definition.syncSchema !== false) {
|
|
70
|
-
const storage = definition.storage || {};
|
|
71
|
-
api.syncSchema(definition.schema, storage.table || name, {
|
|
72
|
-
connection: storage.connection || 'main'
|
|
73
|
-
}).catch(err => {
|
|
74
|
-
console.error(`Failed to sync schema for ${name}:`, err);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return api;
|
|
79
|
-
}
|