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
@@ -0,0 +1,132 @@
1
+ import { Api } from 'hooked-api';
2
+ import { RestApiPlugin, FileHandlingPlugin, ExpressPlugin } from 'json-rest-api';
3
+ import { LocalStorage } from 'json-rest-api';
4
+ import express from 'express';
5
+ import { exec } from 'child_process';
6
+ import { promisify } from 'util';
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+
10
+ const execAsync = promisify(exec);
11
+
12
+ const api = new Api({
13
+ name: 'my-library-api',
14
+ version: '1.0.0'
15
+ });
16
+
17
+ // Create a storage adapter
18
+ const coverStorage = new LocalStorage({
19
+ directory: './uploads/covers',
20
+ baseUrl: 'http://localhost:3000/uploads/covers'
21
+ });
22
+
23
+ // Add plugins (ORDER MATTERS!)
24
+ api.use(RestApiPlugin);
25
+ api.use(FileHandlingPlugin); // Must come after RestApiPlugin
26
+ api.use(ExpressPlugin); // Must come after FileHandlingPlugin
27
+
28
+ // Update the books schema to include a cover image
29
+ api.addResource('books', {
30
+ schema: {
31
+ title: { type: 'string', required: true },
32
+ author: { type: 'string', required: true },
33
+ year: { type: 'number' },
34
+ isbn: { type: 'string' },
35
+ cover: {
36
+ type: 'file',
37
+ storage: coverStorage,
38
+ accepts: ['image/jpeg', 'image/png', 'image/gif'],
39
+ maxSize: '5mb'
40
+ }
41
+ }
42
+ });
43
+
44
+ // Add dataPost helper to handle the cover URL
45
+ api.customize({
46
+ vars: {
47
+ helpers: {
48
+ dataPost: async ({ scopeName, inputRecord }) => {
49
+ const newBook = {
50
+ id: String(Date.now()),
51
+ ...inputRecord.data.attributes
52
+ };
53
+
54
+ // The file has already been uploaded and replaced with a URL
55
+ console.log('Book cover URL:', newBook.cover);
56
+
57
+ return {
58
+ data: {
59
+ type: 'books',
60
+ id: newBook.id,
61
+ attributes: {
62
+ title: newBook.title,
63
+ author: newBook.author,
64
+ year: newBook.year,
65
+ cover: newBook.cover // This is now a URL!
66
+ }
67
+ }
68
+ };
69
+ },
70
+
71
+ dataQuery: async () => ({ data: [] })
72
+ }
73
+ }
74
+ });
75
+
76
+ // Wait for async plugin initialization
77
+ await new Promise(resolve => setTimeout(resolve, 100));
78
+
79
+ // Create Express app
80
+ const app = express();
81
+
82
+ // Serve uploaded files
83
+ app.use('/uploads', express.static('./uploads'));
84
+
85
+ // Mount the API
86
+ app.use(api.http.express.router);
87
+
88
+ const server = app.listen(3000, async () => {
89
+ console.log('✓ File upload API running at http://localhost:3000/api');
90
+
91
+ // Create a test image file
92
+ const testImagePath = './test-cover.png';
93
+ await fs.writeFile(testImagePath, Buffer.from('PNG_TEST_DATA'));
94
+
95
+ try {
96
+ // Test file upload with curl
97
+ console.log('\nTesting file upload with multipart/form-data...\n');
98
+
99
+ const { stdout, stderr } = await execAsync(`curl -s -X POST http://localhost:3000/api/books \
100
+ -F "title=The Hobbit" \
101
+ -F "author=J.R.R. Tolkien" \
102
+ -F "year=1937" \
103
+ -F "cover=@${testImagePath}"`);
104
+
105
+ if (stderr) {
106
+ console.error('Curl error:', stderr);
107
+ } else {
108
+ console.log('Response:', stdout);
109
+
110
+ // Parse response to check if it worked
111
+ try {
112
+ const response = JSON.parse(stdout);
113
+ if (response.data && response.data.attributes && response.data.attributes.cover) {
114
+ console.log('\n✓ File upload successful!');
115
+ console.log(' Book ID:', response.data.id);
116
+ console.log(' Cover URL:', response.data.attributes.cover);
117
+ } else if (response.errors) {
118
+ console.log('\n✗ File upload failed with errors:', response.errors);
119
+ }
120
+ } catch (e) {
121
+ console.log('\n✗ Could not parse response');
122
+ }
123
+ }
124
+ } catch (error) {
125
+ console.error('Test failed:', error.message);
126
+ } finally {
127
+ // Cleanup
128
+ await fs.unlink(testImagePath).catch(() => {});
129
+ server.close();
130
+ process.exit(0);
131
+ }
132
+ });
@@ -0,0 +1,159 @@
1
+ import { Api } from 'hooked-api';
2
+ import { RestApiPlugin, FileHandlingPlugin, HttpPlugin } from 'json-rest-api';
3
+
4
+ const api = new Api({
5
+ name: 'my-library-api',
6
+ version: '1.0.0'
7
+ });
8
+
9
+ // Add plugins
10
+ api.use(RestApiPlugin);
11
+ api.use(FileHandlingPlugin);
12
+ api.use(HttpPlugin, {
13
+ port: 3000, // Server port (default: 3000)
14
+ basePath: '/api', // API base path (default: '/api')
15
+ strictContentType: true, // Enforce JSON content types (default: true)
16
+ requestSizeLimit: '10mb' // Max request body size (default: '1mb')
17
+ });
18
+
19
+ // Add your books resource
20
+ api.addResource('books', {
21
+ schema: {
22
+ title: { type: 'string', required: true },
23
+ author: { type: 'string', required: true },
24
+ year: { type: 'number' },
25
+ isbn: { type: 'string' }
26
+ }
27
+ });
28
+
29
+ // Add simple data helpers
30
+ api.customize({
31
+ vars: {
32
+ helpers: {
33
+ dataQuery: async ({ scopeName, queryParams }) => {
34
+ const fakeBooks = [
35
+ { id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 },
36
+ { id: '2', title: '1984', author: 'George Orwell', year: 1949 },
37
+ { id: '3', title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 }
38
+ ];
39
+
40
+ return {
41
+ data: fakeBooks.map(book => ({
42
+ type: 'books',
43
+ id: book.id,
44
+ attributes: {
45
+ title: book.title,
46
+ author: book.author,
47
+ year: book.year
48
+ }
49
+ }))
50
+ };
51
+ },
52
+
53
+ dataGet: async ({ scopeName, id }) => {
54
+ const book = { id, title: 'Example Book', author: 'Example Author', year: 2024 };
55
+
56
+ return {
57
+ data: {
58
+ type: 'books',
59
+ id: book.id,
60
+ attributes: {
61
+ title: book.title,
62
+ author: book.author,
63
+ year: book.year
64
+ }
65
+ }
66
+ };
67
+ },
68
+
69
+ dataPost: async ({ scopeName, inputRecord }) => {
70
+ const newBook = {
71
+ id: String(Date.now()),
72
+ ...inputRecord.data.attributes
73
+ };
74
+
75
+ return {
76
+ data: {
77
+ type: 'books',
78
+ id: newBook.id,
79
+ attributes: {
80
+ title: newBook.title,
81
+ author: newBook.author,
82
+ year: newBook.year
83
+ }
84
+ }
85
+ };
86
+ },
87
+
88
+ dataPut: async ({ scopeName, id, inputRecord }) => {
89
+ return {
90
+ data: {
91
+ type: 'books',
92
+ id: id,
93
+ attributes: inputRecord.data.attributes
94
+ }
95
+ };
96
+ },
97
+
98
+ dataPatch: async ({ scopeName, id, inputRecord }) => {
99
+ return {
100
+ data: {
101
+ type: 'books',
102
+ id: id,
103
+ attributes: inputRecord.data.attributes
104
+ }
105
+ };
106
+ },
107
+
108
+ dataDelete: async ({ scopeName, id }) => {
109
+ return { success: true };
110
+ }
111
+ }
112
+ }
113
+ });
114
+
115
+ // Wait for async plugin installation to complete
116
+ await new Promise(resolve => setTimeout(resolve, 100));
117
+
118
+ // Start the HTTP server - multiple options:
119
+ console.log('Testing HTTP plugin options...\n');
120
+
121
+ // Check if api.http namespace exists
122
+ console.log('api.http exists?', !!api.http);
123
+ console.log('api.http.startServer exists?', typeof api.http?.startServer);
124
+
125
+ // Option 1: Start on configured port (3000 by default)
126
+ api.http.startServer();
127
+
128
+ console.log('✓ HTTP server started successfully!');
129
+ console.log(' API running at http://localhost:3000/api');
130
+
131
+ // Test with curl commands after a short delay
132
+ import { exec } from 'child_process';
133
+ import { promisify } from 'util';
134
+ const execAsync = promisify(exec);
135
+
136
+ setTimeout(async () => {
137
+ try {
138
+ console.log('\nTesting endpoints...\n');
139
+
140
+ // Test GET all books
141
+ console.log('1. Testing GET /api/books:');
142
+ const { stdout: getAll } = await execAsync('curl -s http://localhost:3000/api/books');
143
+ console.log('✓ Response:', getAll.substring(0, 100) + '...');
144
+
145
+ // Test GET single book
146
+ console.log('\n2. Testing GET /api/books/1:');
147
+ const { stdout: getOne } = await execAsync('curl -s http://localhost:3000/api/books/1');
148
+ console.log('✓ Response:', getOne.substring(0, 100) + '...');
149
+
150
+ // Close server
151
+ console.log('\nClosing server...');
152
+ api.http.server.close();
153
+ process.exit(0);
154
+
155
+ } catch (error) {
156
+ console.error('Test error:', error.message);
157
+ process.exit(1);
158
+ }
159
+ }, 500);
@@ -0,0 +1,188 @@
1
+ import { Api } from 'hooked-api';
2
+ import { RestApiPlugin } from 'json-rest-api';
3
+
4
+ const api = new Api({
5
+ name: 'my-library-api',
6
+ version: '1.0.0'
7
+ });
8
+
9
+ // Use the REST API plugin
10
+ api.use(RestApiPlugin);
11
+
12
+ // Define a books resource
13
+ api.addResource('books', {
14
+ schema: {
15
+ title: { type: 'string', required: true },
16
+ author: { type: 'string', required: true },
17
+ year: { type: 'number' },
18
+ isbn: { type: 'string' }
19
+ }
20
+ });
21
+
22
+ // Add simple data helpers
23
+ api.customize({
24
+ vars: {
25
+ helpers: {
26
+ dataQuery: async ({ scopeName, queryParams }) => {
27
+ const fakeBooks = [
28
+ { id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 },
29
+ { id: '2', title: '1984', author: 'George Orwell', year: 1949 },
30
+ { id: '3', title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 }
31
+ ];
32
+
33
+ return {
34
+ data: fakeBooks.map(book => ({
35
+ type: 'books',
36
+ id: book.id,
37
+ attributes: {
38
+ title: book.title,
39
+ author: book.author,
40
+ year: book.year
41
+ }
42
+ }))
43
+ };
44
+ },
45
+
46
+ dataGet: async ({ scopeName, id }) => {
47
+ const book = { id, title: 'Example Book', author: 'Example Author', year: 2024 };
48
+
49
+ return {
50
+ data: {
51
+ type: 'books',
52
+ id: book.id,
53
+ attributes: {
54
+ title: book.title,
55
+ author: book.author,
56
+ year: book.year
57
+ }
58
+ }
59
+ };
60
+ },
61
+
62
+ dataPost: async ({ scopeName, inputRecord }) => {
63
+ const newBook = {
64
+ id: String(Date.now()),
65
+ ...inputRecord.data.attributes
66
+ };
67
+
68
+ return {
69
+ data: {
70
+ type: 'books',
71
+ id: newBook.id,
72
+ attributes: {
73
+ title: newBook.title,
74
+ author: newBook.author,
75
+ year: newBook.year
76
+ }
77
+ }
78
+ };
79
+ },
80
+
81
+ dataPut: async ({ scopeName, id, inputRecord }) => {
82
+ return {
83
+ data: {
84
+ type: 'books',
85
+ id: id,
86
+ attributes: inputRecord.data.attributes
87
+ }
88
+ };
89
+ },
90
+
91
+ dataPatch: async ({ scopeName, id, inputRecord }) => {
92
+ return {
93
+ data: {
94
+ type: 'books',
95
+ id: id,
96
+ attributes: inputRecord.data.attributes
97
+ }
98
+ };
99
+ },
100
+
101
+ dataDelete: async ({ scopeName, id }) => {
102
+ return { success: true };
103
+ }
104
+ }
105
+ }
106
+ });
107
+
108
+ // Test programmatic API usage
109
+ console.log('Testing programmatic API usage...\n');
110
+
111
+ // First check what methods are available
112
+ console.log('Available methods on api.resources.books:');
113
+ if (api.resources && api.resources.books) {
114
+ const methods = Object.keys(api.resources.books).filter(k => typeof api.resources.books[k] === 'function');
115
+ console.log(' Methods:', methods.join(', '));
116
+ } else {
117
+ console.log(' ERROR: api.resources.books is not available');
118
+ }
119
+
120
+ try {
121
+ // Query all books
122
+ console.log('\n1. Testing query():');
123
+ const allBooks = await api.resources.books.query({});
124
+ console.log('✓ Query successful:', JSON.stringify(allBooks, null, 2));
125
+
126
+ // Get a single book
127
+ console.log('\n2. Testing get():');
128
+ const book = await api.resources.books.get({ id: '1' });
129
+ console.log('✓ Get successful:', JSON.stringify(book, null, 2));
130
+
131
+ // Create a new book
132
+ console.log('\n3. Testing post():');
133
+ const newBook = await api.resources.books.post({
134
+ inputRecord: {
135
+ data: {
136
+ type: 'books',
137
+ attributes: {
138
+ title: 'New Book',
139
+ author: 'New Author',
140
+ year: 2024
141
+ }
142
+ }
143
+ }
144
+ });
145
+ console.log('✓ Post successful:', JSON.stringify(newBook, null, 2));
146
+
147
+ // Update a book (replace all fields)
148
+ console.log('\n4. Testing put():');
149
+ const updatedBook = await api.resources.books.put({
150
+ id: '1',
151
+ inputRecord: {
152
+ data: {
153
+ type: 'books',
154
+ attributes: {
155
+ title: 'Updated Title',
156
+ author: 'Updated Author',
157
+ year: 2024
158
+ }
159
+ }
160
+ }
161
+ });
162
+ console.log('✓ Put successful:', JSON.stringify(updatedBook, null, 2));
163
+
164
+ // Partially update a book
165
+ console.log('\n5. Testing patch():');
166
+ const patchedBook = await api.resources.books.patch({
167
+ id: '1',
168
+ inputRecord: {
169
+ data: {
170
+ type: 'books',
171
+ attributes: {
172
+ title: 'New Title Only'
173
+ }
174
+ }
175
+ }
176
+ });
177
+ console.log('✓ Patch successful:', JSON.stringify(patchedBook, null, 2));
178
+
179
+ // Delete a book
180
+ console.log('\n6. Testing delete():');
181
+ const deleteResult = await api.resources.books.delete({ id: '1' });
182
+ console.log('✓ Delete successful:', JSON.stringify(deleteResult, null, 2));
183
+
184
+ console.log('\n✅ All programmatic API tests passed!');
185
+ } catch (error) {
186
+ console.error('❌ Error:', error.message);
187
+ console.error(error.stack);
188
+ }
@@ -0,0 +1,13 @@
1
+ import { Api } from 'hooked-api';
2
+ import { RestApiPlugin } from 'json-rest-api';
3
+
4
+ const api = new Api({
5
+ name: 'my-library-api',
6
+ version: '1.0.0'
7
+ });
8
+
9
+ // Use the REST API plugin
10
+ api.use(RestApiPlugin);
11
+
12
+ console.log('✓ REST API Plugin loaded successfully!');
13
+ console.log(' Plugin registered:', api.plugins.has(RestApiPlugin));
package/TODO.todo DELETED
@@ -1,113 +0,0 @@
1
- TODO
2
-
3
- 10. Utility Features
4
-
5
- - Missing from original:
6
- - artificial delay for testing <-- ADD!!!
7
- - Request decoration pattern <-- I DON'T KNOW WHAT THIS IS, BUT IF IT'S BEST PRACTICE/INDUSTRU STANDARD, TELL ME ABOUT IT
8
- - Store inheritance/extension <--- THIS, I NEVER WANTED, NEVER USED IN 10 YEARS IN PRODUCTION
9
- - Automatic timestamp fields <-- WOULD BE NICE
10
- - Soft delete support <-- WOULD BE NICE BUT I DON'T THINK THE ORIGINAL HAD IT. I WILL IMPLEMENT IT IN THE FUTURE
11
-
12
-
13
- 5. Error Handling
14
-
15
- - Missing structured error classes:
16
- - BadRequest, NotFound, Conflict classes
17
- - Error code standardization
18
- - Multi-field validation errors
19
- - Error context preservation
20
-
21
- 6. Hook System Gaps
22
-
23
- - Missing hooks from original:
24
- - beforeId, afterId
25
- - beforeRetrieve, afterRetrieve
26
- - beforeInsert, afterInsert (full lifecycle)
27
- - queryFieldsAndJoins
28
- - updateConditionsAndArgs
29
- - deleteConditionsAndArgs
30
-
31
- 7. Store Templates
32
-
33
- - No equivalent to storeTemplate.js:
34
- - Automatic REST endpoint generation
35
- - Built-in authentication handling
36
- - Session management
37
- - Request/response transformation
38
-
39
- 8. Memory Store Features
40
-
41
- - Current memory plugin lacks:
42
- - Query operators ($like, $in, $gt, etc.)
43
- - Sorting by multiple fields
44
- - Limit/offset with total count
45
- - Deep object filtering
46
-
47
-
48
-
49
- 11. JSON:API Compliance Gaps
50
-
51
- - Current HTTP plugin missing:
52
- - Proper relationship handling
53
- - Compound documents
54
- - Sparse fieldsets
55
- - Include parameter processing
56
- - Meta information structure
57
-
58
- 12. Performance Features
59
-
60
- - Not implemented:
61
- - Connection pooling configuration
62
- - Query result caching
63
- - Batch operations
64
- - Transaction support across operations
65
-
66
-
67
-
68
- **************************************************************************
69
- DONE
70
- **************************************************************************
71
-
72
- 2. MySQL Schema Synchronization
73
-
74
- - Current MySQL plugin is basic, missing:
75
- - Automatic table creation/modification
76
- - Column type mapping from schema
77
- - Index creation for searchable fields
78
- - Foreign key constraint management
79
- - Schema migration tracking
80
-
81
- 9. Schema Features
82
-
83
- - SimplSchema integration missing:
84
- - onlyObjectValues for partial updates
85
- - Conditional defaults based on other fields
86
- - Array validation with item schemas
87
- - Custom error messages per validator
88
-
89
- 1. Positioning System
90
-
91
- - Current has basic positioning plugin but lacks:
92
- - Filtering support during repositioning
93
- - Gap-free positioning algorithm
94
- - Complex position calculations with conditions
95
- - Batch repositioning operations
96
-
97
- 3. Advanced Querying
98
-
99
- - Missing query capabilities:
100
- - Complex field joins (queryFieldsAndJoins hook)
101
- - Nested object queries
102
- - Array field queries
103
- - Custom SQL conditions
104
- - Aggregation support
105
-
106
- 4. Versioning Management
107
-
108
- - Current versioning is rudimentary, missing:
109
- - Version range negotiation
110
- - Store registry with semver matching
111
- - Version-specific schema transformations
112
- - Backward compatibility helpers
113
-