json-rest-api 1.0.0 → 1.0.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/.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
|
@@ -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
|
-
|