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.
Files changed (67) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/LICENSE +674 -0
  3. package/README.md +655 -309
  4. package/RELATIONSHIPS.md +1013 -0
  5. package/TODO_rest-api-plugin-extras.js +142 -0
  6. package/connectors_ideas/express-plugin.js +120 -0
  7. package/connectors_ideas/fastify.js +219 -0
  8. package/connectors_ideas/http-plugin.js +138 -0
  9. package/connectors_ideas/koa-plugin.js +113 -0
  10. package/docs/fileUploads.md +722 -0
  11. package/docs/mini-guides/file-uploads-complete.md +159 -0
  12. package/examples/.gitkeep +0 -0
  13. package/examples/express-example.js +262 -0
  14. package/examples/express-plugin.js +366 -0
  15. package/examples/file-upload-example.js +205 -0
  16. package/examples/filename-strategies.js +140 -0
  17. package/examples/knex-basic.js +66 -0
  18. package/index.js +31 -56
  19. package/lib/payload-validators.js +685 -0
  20. package/lib/rest-api-errors.js +58 -0
  21. package/package.json +41 -14
  22. package/payloadValdators.js +686 -0
  23. package/plugins/core/connectors/express-plugin.js +449 -0
  24. package/plugins/core/connectors/http-plugin.js +450 -0
  25. package/plugins/core/file-handling-plugin.js +338 -0
  26. package/plugins/core/lib/busboy-detector.js +167 -0
  27. package/plugins/core/lib/formidable-detector.js +130 -0
  28. package/plugins/core/rest-api-knex-plugin.js +200 -0
  29. package/plugins/core/rest-api-plugin.js +1419 -0
  30. package/plugins/storage/local-storage.js +218 -0
  31. package/plugins/storage/s3-storage.js +125 -0
  32. package/test-examples/package-lock.json +906 -0
  33. package/test-examples/package.json +15 -0
  34. package/test-examples/test-basic-api.js +11 -0
  35. package/test-examples/test-books-resource.js +116 -0
  36. package/test-examples/test-complete-file-upload.js +140 -0
  37. package/test-examples/test-debug.js +28 -0
  38. package/test-examples/test-express-plugin.js +177 -0
  39. package/test-examples/test-express-simple.js +28 -0
  40. package/test-examples/test-express-with-import.js +71 -0
  41. package/test-examples/test-file-upload.js +132 -0
  42. package/test-examples/test-http-plugin.js +159 -0
  43. package/test-examples/test-programmatic-api.js +188 -0
  44. package/test-examples/test-rest-api-plugin.js +13 -0
  45. package/TODO.todo +0 -113
  46. package/api.js +0 -628
  47. package/docs/API.md +0 -1306
  48. package/docs/FIXES_APPLIED.md +0 -59
  49. package/docs/GUIDES.md +0 -1792
  50. package/docs/RESOURCES_IN_EXPRESS.md +0 -456
  51. package/docs/example-versioning.js +0 -168
  52. package/docs/example.js +0 -195
  53. package/plugins/API guide.md +0 -364
  54. package/plugins/api-registry.js +0 -142
  55. package/plugins/api-versioning.md +0 -135
  56. package/plugins/http.js +0 -273
  57. package/plugins/logging.js +0 -255
  58. package/plugins/memory.js +0 -182
  59. package/plugins/mysql.js +0 -753
  60. package/plugins/openapi.js +0 -401
  61. package/plugins/positioning.js +0 -220
  62. package/plugins/security.js +0 -261
  63. package/plugins/validation.js +0 -173
  64. package/plugins/versioning.js +0 -288
  65. package/resource-helper.js +0 -79
  66. package/schema.js +0 -481
  67. package/test-basic.js +0 -107
package/docs/example.js DELETED
@@ -1,195 +0,0 @@
1
- import express from 'express';
2
- import {
3
- createApi,
4
- Api,
5
- Schema,
6
- MySQLPlugin,
7
- HTTPPlugin,
8
- ValidationPlugin,
9
- PositioningPlugin,
10
- VersioningPlugin
11
- } from './index.js';
12
-
13
- // Example 1: Simple API with memory storage
14
- const simpleApi = createApi({
15
- storage: 'memory',
16
- http: {
17
- basePath: '/api/v1',
18
- typeOptions: {
19
- users: {
20
- searchFields: ['name', 'email']
21
- }
22
- }
23
- }
24
- });
25
-
26
- // Define a schema
27
- const userSchema = new Schema({
28
- id: { type: 'id' },
29
- name: { type: 'string', required: true, min: 2, max: 100 },
30
- email: { type: 'string', required: true, lowercase: true },
31
- age: { type: 'number', min: 0, max: 150 },
32
- active: { type: 'boolean', default: true },
33
- createdAt: { type: 'timestamp', default: () => Date.now() }
34
- });
35
-
36
- // Register the schema
37
- simpleApi.addResource('users', userSchema);
38
-
39
- // Example 2: Advanced API with MySQL and all features
40
- const advancedApi = new Api({ idProperty: 'id' });
41
-
42
- // Configure plugins
43
- advancedApi
44
- .use(ValidationPlugin)
45
- .use(MySQLPlugin, {
46
- connections: [{
47
- name: 'main',
48
- config: {
49
- host: 'localhost',
50
- user: 'root',
51
- password: 'password',
52
- database: 'myapp'
53
- }
54
- }]
55
- })
56
- .use(PositioningPlugin, {
57
- positionField: 'position',
58
- beforeIdField: 'beforeId'
59
- })
60
- .use(VersioningPlugin, {
61
- apiVersion: '2.0.0',
62
- trackHistory: true,
63
- optimisticLocking: true
64
- })
65
- .use(HTTPPlugin, {
66
- basePath: '/api/v2'
67
- });
68
-
69
- // Define schemas
70
- const productSchema = new Schema({
71
- id: { type: 'id' },
72
- name: { type: 'string', required: true, min: 1, max: 200 },
73
- description: { type: 'string', max: 1000 },
74
- price: { type: 'number', required: true, min: 0, currency: true },
75
- stock: { type: 'number', default: 0, min: 0 },
76
- category: { type: 'string', required: true },
77
- tags: { type: 'array', default: [] },
78
- metadata: { type: 'object' },
79
- active: { type: 'boolean', default: true },
80
- position: { type: 'number', default: 0 },
81
- version: { type: 'number', default: 1 },
82
- createdAt: { type: 'timestamp', default: () => Date.now() },
83
- updatedAt: { type: 'timestamp' }
84
- });
85
-
86
- // Create search schema from main schema
87
- const productSearchSchema = advancedApi.createSearchSchema(productSchema, [
88
- 'name', 'category', 'active'
89
- ]);
90
-
91
- // Register schemas
92
- advancedApi.addResource('products', productSchema);
93
-
94
- // Custom validation hook
95
- advancedApi.hook('afterValidate', async (context) => {
96
- if (context.method === 'insert' || context.method === 'update') {
97
- const { data } = context;
98
-
99
- // Custom business rule: expensive products must have description
100
- if (data.price > 1000 && !data.description) {
101
- context.errors.push({
102
- field: 'description',
103
- message: 'Products over $1000 must have a description',
104
- code: 'EXPENSIVE_PRODUCT_NEEDS_DESCRIPTION'
105
- });
106
- }
107
- }
108
- });
109
-
110
- // Transform hook to add computed fields
111
- advancedApi.hook('transformResult', async (context) => {
112
- if (context.result && context.options.type === 'products') {
113
- const product = context.result;
114
-
115
- // Add computed field
116
- product.displayPrice = `$${(product.price || 0).toFixed(2)}`;
117
- product.inStock = product.stock > 0;
118
- }
119
- });
120
-
121
- // Example 3: Using the API programmatically
122
- async function exampleUsage() {
123
- // Insert a product
124
- const newProduct = await advancedApi.insert({
125
- name: 'Premium Widget',
126
- description: 'A high-quality widget for professionals',
127
- price: 1299.99,
128
- stock: 50,
129
- category: 'widgets',
130
- tags: ['premium', 'professional'],
131
- beforeId: null // Will be placed at the end
132
- }, {
133
- type: 'products',
134
- table: 'products',
135
- positioning: { enabled: true }
136
- });
137
-
138
- console.log('Created product:', newProduct);
139
-
140
- // Query products
141
- const products = await advancedApi.query({
142
- filter: { category: 'widgets', active: true },
143
- sort: '-price,name',
144
- page: { size: 10, number: 1 }
145
- }, {
146
- type: 'products',
147
- table: 'products'
148
- });
149
-
150
- console.log('Found products:', products);
151
-
152
- // Update with optimistic locking
153
- const updated = await advancedApi.update(newProduct.data.id, {
154
- stock: 45,
155
- version: newProduct.data.attributes.version // For optimistic locking
156
- }, {
157
- type: 'products',
158
- table: 'products'
159
- });
160
-
161
- console.log('Updated product:', updated);
162
-
163
- // Get version history
164
- const history = await advancedApi.getVersionHistory('products', newProduct.data.id);
165
- console.log('Version history:', history);
166
- }
167
-
168
- // Example 4: Express app setup
169
- const app = express();
170
-
171
- // Mount the APIs
172
- simpleApi.mount(app);
173
- advancedApi.mount(app);
174
-
175
- // Add custom middleware
176
- advancedApi.useMiddleware((req, res, next) => {
177
- // Add user ID from session for versioning
178
- req.options = { userId: req.session?.userId };
179
- next();
180
- });
181
-
182
- // Sync database schema
183
- async function syncDatabase() {
184
- await advancedApi.syncSchema(productSchema, 'products', {
185
- connection: 'main'
186
- });
187
- }
188
-
189
- // Start server
190
- app.listen(3000, () => {
191
- console.log('API server running on http://localhost:3000');
192
- });
193
-
194
- // Export for use in other modules
195
- export { simpleApi, advancedApi, userSchema, productSchema };
@@ -1,364 +0,0 @@
1
- ● Programmatic API Usage Guide
2
-
3
- Here's how to use the JSON REST API methods programmatically:
4
-
5
- Basic Setup
6
-
7
- import { createApi, Schema } from './jsonrestapi/index.js';
8
-
9
- // Create API instance
10
- const api = createApi({
11
- storage: 'memory', // or 'mysql'
12
- http: false // Disable HTTP if using programmatically only
13
- });
14
-
15
- // Define a schema
16
- const userSchema = new Schema({
17
- id: { type: 'id' },
18
- name: { type: 'string', required: true },
19
- email: { type: 'string', required: true, lowercase: true },
20
- age: { type: 'number', min: 0 },
21
- active: { type: 'boolean', default: true }
22
- });
23
-
24
- // Register schema
25
- api.addResource('users', userSchema);
26
-
27
- CRUD Operations
28
-
29
- 1. Create (Insert)
30
-
31
- // Simple insert
32
- const newUser = await api.insert({
33
- name: 'John Doe',
34
- email: 'john@example.com',
35
- age: 30
36
- }, {
37
- type: 'users'
38
- });
39
-
40
- console.log(newUser);
41
- // Output: { data: { id: '1', type: 'users', attributes: { name: 'John
42
- Doe', ... } } }
43
-
44
- // Insert with positioning (if PositioningPlugin is used)
45
- const secondUser = await api.insert({
46
- name: 'Jane Smith',
47
- email: 'jane@example.com',
48
- beforeId: null // Place at end
49
- }, {
50
- type: 'users',
51
- positioning: { enabled: true }
52
- });
53
-
54
- 2. Read (Get)
55
-
56
- // Get by ID
57
- const user = await api.get('1', {
58
- type: 'users'
59
- });
60
-
61
- console.log(user);
62
- // Output: { data: { id: '1', type: 'users', attributes: { ... } } }
63
-
64
- // Handle not found
65
- try {
66
- const notFound = await api.get('999', { type: 'users' });
67
- } catch (error) {
68
- console.log('User not found');
69
- }
70
-
71
- 3. Update
72
-
73
- // Partial update
74
- const updated = await api.update('1', {
75
- age: 31,
76
- active: false
77
- }, {
78
- type: 'users'
79
- });
80
-
81
- // Update with validation context
82
- const updateWithContext = await api.update('1', {
83
- email: 'newemail@example.com'
84
- }, {
85
- type: 'users',
86
- userId: 'admin-123', // For audit logging
87
- ipAddress: '192.168.1.1'
88
- });
89
-
90
- 4. Delete
91
-
92
- // Delete by ID
93
- await api.delete('1', {
94
- type: 'users'
95
- });
96
-
97
- // Delete returns { data: null } on success
98
-
99
- 5. Query (List)
100
-
101
- // Simple query
102
- const allUsers = await api.query({}, {
103
- type: 'users'
104
- });
105
-
106
- // Advanced query with filters
107
- const activeAdults = await api.query({
108
- filter: {
109
- active: true,
110
- age: { $gte: 18 } // If your storage plugin supports operators
111
- },
112
- sort: '-age,name', // Sort by age DESC, then name ASC
113
- page: {
114
- size: 20,
115
- number: 1
116
- }
117
- }, {
118
- type: 'users'
119
- });
120
-
121
- console.log(activeAdults);
122
- // Output: {
123
- // data: [ { id: '1', type: 'users', attributes: {...} }, ... ],
124
- // meta: { total: 45, pageSize: 20, pageNumber: 1, totalPages: 3 },
125
- // links: { self: '...', next: '...', last: '...' }
126
- // }
127
-
128
- // Search (if configured)
129
- const searchResults = await api.query({
130
- search: 'john',
131
- filter: { active: true }
132
- }, {
133
- type: 'users',
134
- searchFields: ['name', 'email']
135
- });
136
-
137
- Advanced Usage
138
-
139
- With MySQL Storage
140
-
141
- import { Api, MySQLPlugin, ValidationPlugin } from
142
- './jsonrestapi/index.js';
143
-
144
- const api = new Api()
145
- .use(ValidationPlugin)
146
- .use(MySQLPlugin, {
147
- connection: {
148
- host: 'localhost',
149
- user: 'root',
150
- password: 'password',
151
- database: 'myapp'
152
- }
153
- });
154
-
155
- // Sync schema with database
156
- await api.syncSchema(userSchema, 'users');
157
-
158
- // Use with table specification
159
- const user = await api.insert({
160
- name: 'Database User',
161
- email: 'db@example.com'
162
- }, {
163
- type: 'users',
164
- table: 'users', // MySQL table name
165
- connection: 'default'
166
- });
167
-
168
- With Authentication Context
169
-
170
- // Add security plugin
171
- api.use(SecurityPlugin);
172
-
173
- // All operations can include auth context
174
- const result = await api.insert({
175
- title: 'Secure Post',
176
- content: 'This is secure'
177
- }, {
178
- type: 'posts',
179
- userId: 'user-123', // From decoded JWT
180
- authenticated: true,
181
- permissions: ['create', 'read']
182
- });
183
-
184
- Batch Operations
185
-
186
- // Insert multiple records
187
- const users = [
188
- { name: 'User 1', email: 'user1@example.com' },
189
- { name: 'User 2', email: 'user2@example.com' },
190
- { name: 'User 3', email: 'user3@example.com' }
191
- ];
192
-
193
- const created = await Promise.all(
194
- users.map(user => api.insert(user, { type: 'users' }))
195
- );
196
-
197
- // Update multiple records
198
- const updates = [
199
- { id: '1', data: { active: false } },
200
- { id: '2', data: { active: false } },
201
- { id: '3', data: { active: false } }
202
- ];
203
-
204
- await Promise.all(
205
- updates.map(({ id, data }) =>
206
- api.update(id, data, { type: 'users' })
207
- )
208
- );
209
-
210
- With Transactions (MySQL)
211
-
212
- // Get connection pool
213
- const { pool } = api.getConnection();
214
- const connection = await pool.getConnection();
215
-
216
- try {
217
- await connection.beginTransaction();
218
-
219
- // Create user
220
- const user = await api.insert({
221
- name: 'Transaction User',
222
- email: 'tx@example.com'
223
- }, {
224
- type: 'users',
225
- connection // Pass connection for transaction
226
- });
227
-
228
- // Create related profile
229
- await api.insert({
230
- userId: user.data.id,
231
- bio: 'Created in transaction'
232
- }, {
233
- type: 'profiles',
234
- connection
235
- });
236
-
237
- await connection.commit();
238
- } catch (error) {
239
- await connection.rollback();
240
- throw error;
241
- } finally {
242
- connection.release();
243
- }
244
-
245
- With Custom Validation
246
-
247
- // Add custom validation hook
248
- api.hook('afterValidate', async (context) => {
249
- if (context.options.type === 'users') {
250
- const { data, method } = context;
251
-
252
- // Check email uniqueness
253
- if ((method === 'insert' || method === 'update') && data.email) {
254
- const existing = await api.query({
255
- filter: { email: data.email }
256
- }, { type: 'users' });
257
-
258
- if (existing.data.length > 0) {
259
- context.errors.push({
260
- field: 'email',
261
- message: 'Email already exists',
262
- code: 'DUPLICATE_EMAIL'
263
- });
264
- }
265
- }
266
- }
267
- });
268
-
269
- Error Handling
270
-
271
- try {
272
- const user = await api.insert({
273
- name: 'J', // Too short!
274
- email: 'invalid-email'
275
- }, { type: 'users' });
276
- } catch (error) {
277
- if (error.errors) {
278
- // Validation errors
279
- error.errors.forEach(err => {
280
- console.log(`${err.field}: ${err.message}`);
281
- });
282
- } else {
283
- // Other errors
284
- console.error('Unexpected error:', error);
285
- }
286
- }
287
-
288
- Direct Storage Access
289
-
290
- // If you need direct access to storage
291
- if (api.memoryData) {
292
- // Direct memory access
293
- console.log('All data:', api.memoryData);
294
- }
295
-
296
- if (api.mysqlPools) {
297
- // Direct MySQL access
298
- const { pool } = api.getConnection();
299
- const [rows] = await pool.query('SELECT * FROM users WHERE active = ?',
300
- [true]);
301
- }
302
-
303
- Complete Example: Task Manager
304
-
305
- import { createApi, Schema, PositioningPlugin } from
306
- './jsonrestapi/index.js';
307
-
308
- // Create API
309
- const api = createApi({ storage: 'memory' })
310
- .use(PositioningPlugin);
311
-
312
- // Define schema
313
- const taskSchema = new Schema({
314
- id: { type: 'id' },
315
- title: { type: 'string', required: true, min: 1, max: 200 },
316
- description: { type: 'string', max: 1000 },
317
- completed: { type: 'boolean', default: false },
318
- priority: { type: 'string', enum: ['low', 'medium', 'high'], default:
319
- 'medium' },
320
- dueDate: { type: 'date' },
321
- position: { type: 'number' },
322
- createdAt: { type: 'timestamp', default: () => Date.now() }
323
- });
324
-
325
- api.addResource('tasks', taskSchema);
326
-
327
- // Usage
328
- async function taskManager() {
329
- // Create tasks
330
- const task1 = await api.insert({
331
- title: 'Write documentation',
332
- priority: 'high',
333
- dueDate: '2024-12-31'
334
- }, { type: 'tasks' });
335
-
336
- const task2 = await api.insert({
337
- title: 'Review PR',
338
- priority: 'medium',
339
- beforeId: task1.data.id // Position before first task
340
- }, {
341
- type: 'tasks',
342
- positioning: { enabled: true }
343
- });
344
-
345
- // List high priority incomplete tasks
346
- const urgent = await api.query({
347
- filter: {
348
- completed: false,
349
- priority: 'high'
350
- },
351
- sort: 'position,dueDate'
352
- }, { type: 'tasks' });
353
-
354
- console.log(`Found ${urgent.meta.total} urgent tasks`);
355
-
356
- // Complete a task
357
- await api.update(task1.data.id, {
358
- completed: true
359
- }, { type: 'tasks' });
360
-
361
- // Reorder tasks
362
- await api.reposition('tasks', task2.data.id, null); // Move to end
363
- }
364
-
@@ -1,142 +0,0 @@
1
- import semver from 'semver';
2
-
3
- /**
4
- * API Registry Plugin - Automatic version management
5
- */
6
- export const ApiRegistryPlugin = {
7
- install(api, options = {}) {
8
- // Initialize the global registry if it doesn't exist
9
- if (!globalThis.__jsonRestApiRegistry) {
10
- globalThis.__jsonRestApiRegistry = new ApiRegistry();
11
- }
12
-
13
- const registry = globalThis.__jsonRestApiRegistry;
14
-
15
- // Register this API instance
16
- const apiName = options.name || api.options.name;
17
- const apiVersion = options.version || api.options.version;
18
-
19
- if (!apiName || !apiVersion) {
20
- throw new Error('API name and version are required for registration');
21
- }
22
-
23
- api.apiName = apiName;
24
- api.apiVersion = apiVersion;
25
-
26
- // Register in global registry
27
- registry.register(apiName, apiVersion, api);
28
-
29
- // Add version resolution to the API
30
- api.apis = new Proxy({}, {
31
- get(target, prop) {
32
- const requestedVersion = api.apiVersion;
33
- return registry.getCompatibleApi(prop, requestedVersion);
34
- }
35
- });
36
-
37
- // Override HTTP routes to handle version negotiation
38
- if (api.router) {
39
- const originalMount = api.mount;
40
- api.mount = function(app, basePath = '/api') {
41
- // Mount versioned routes
42
- const versionPath = `${basePath}/${apiVersion}`;
43
- originalMount.call(api, app, versionPath);
44
-
45
- // Also mount on base path with version negotiation
46
- app.use(`${basePath}/:apiName/*`, (req, res, next) => {
47
- const requestedVersion =
48
- req.headers['api-version'] ||
49
- req.headers['x-api-version'] ||
50
- req.query.v ||
51
- 'latest';
52
-
53
- const compatibleApi = registry.getCompatibleApi(
54
- req.params.apiName,
55
- requestedVersion
56
- );
57
-
58
- if (!compatibleApi) {
59
- return res.status(400).json({
60
- errors: [{
61
- status: '400',
62
- title: 'Version Not Found',
63
- detail: `No compatible version found for ${req.params.apiName} ${requestedVersion}`
64
- }]
65
- });
66
- }
67
-
68
- // Forward to the compatible API
69
- req.url = req.url.replace(`/${req.params.apiName}`, '');
70
- compatibleApi.router(req, res, next);
71
- });
72
-
73
- return api;
74
- };
75
- }
76
- }
77
- };
78
-
79
- /**
80
- * Global API Registry
81
- */
82
- class ApiRegistry {
83
- constructor() {
84
- this.apis = new Map(); // name -> version -> api
85
- }
86
-
87
- register(name, version, api) {
88
- if (!this.apis.has(name)) {
89
- this.apis.set(name, new Map());
90
- }
91
-
92
- const versions = this.apis.get(name);
93
- versions.set(version, api);
94
-
95
- // Sort versions for efficient lookup
96
- const sortedVersions = Array.from(versions.entries())
97
- .sort(([a], [b]) => semver.compare(b, a));
98
-
99
- this.apis.set(name, new Map(sortedVersions));
100
- }
101
-
102
- getCompatibleApi(name, versionSpec) {
103
- const versions = this.apis.get(name);
104
- if (!versions) return null;
105
-
106
- // Special case: latest
107
- if (versionSpec === 'latest') {
108
- return versions.values().next().value;
109
- }
110
-
111
- // Exact match
112
- if (versions.has(versionSpec)) {
113
- return versions.get(versionSpec);
114
- }
115
-
116
- // Find compatible version
117
- for (const [version, api] of versions) {
118
- if (semver.satisfies(version, versionSpec)) {
119
- return api;
120
- }
121
- }
122
-
123
- // Fallback: find any version >= requested
124
- if (!versionSpec.includes('>') && !versionSpec.includes('<') && !versionSpec.includes('^') && !versionSpec.includes('~')) {
125
- for (const [version, api] of versions) {
126
- if (semver.gte(version, versionSpec)) {
127
- return api;
128
- }
129
- }
130
- }
131
-
132
- return null;
133
- }
134
-
135
- getAllVersions(name) {
136
- const versions = this.apis.get(name);
137
- return versions ? Array.from(versions.keys()) : [];
138
- }
139
- }
140
-
141
- // Export the registry for direct access if needed
142
- export const apiRegistry = globalThis.__jsonRestApiRegistry || new ApiRegistry();