json-rest-api 0.1.1 → 1.0.0

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.
@@ -0,0 +1,364 @@
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
+
@@ -0,0 +1,142 @@
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();
@@ -0,0 +1,135 @@
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!