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.
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
@@ -1,401 +0,0 @@
1
- /**
2
- * OpenAPI (Swagger) documentation plugin
3
- */
4
- export const OpenAPIPlugin = {
5
- install(api, options = {}) {
6
- const defaultOptions = {
7
- title: 'API Documentation',
8
- version: '1.0.0',
9
- description: 'REST API with JSON:API specification',
10
- servers: [{ url: 'http://localhost:3000/api' }],
11
- contact: {},
12
- license: {},
13
- ...options
14
- };
15
-
16
- // Generate OpenAPI specification
17
- api.generateOpenAPISpec = () => {
18
- const spec = {
19
- openapi: '3.0.0',
20
- info: {
21
- title: defaultOptions.title,
22
- version: defaultOptions.version,
23
- description: defaultOptions.description,
24
- contact: defaultOptions.contact,
25
- license: defaultOptions.license
26
- },
27
- servers: defaultOptions.servers,
28
- paths: {},
29
- components: {
30
- schemas: {},
31
- securitySchemes: {
32
- bearerAuth: {
33
- type: 'http',
34
- scheme: 'bearer',
35
- bearerFormat: 'JWT'
36
- },
37
- apiKey: {
38
- type: 'apiKey',
39
- in: 'header',
40
- name: 'X-API-Key'
41
- }
42
- },
43
- parameters: {
44
- idParam: {
45
- name: 'id',
46
- in: 'path',
47
- required: true,
48
- schema: { type: 'string' },
49
- description: 'Resource identifier'
50
- },
51
- typeParam: {
52
- name: 'type',
53
- in: 'path',
54
- required: true,
55
- schema: { type: 'string' },
56
- description: 'Resource type'
57
- }
58
- },
59
- responses: {
60
- NotFound: {
61
- description: 'Resource not found',
62
- content: {
63
- 'application/vnd.api+json': {
64
- schema: { $ref: '#/components/schemas/Error' }
65
- }
66
- }
67
- },
68
- ValidationError: {
69
- description: 'Validation failed',
70
- content: {
71
- 'application/vnd.api+json': {
72
- schema: { $ref: '#/components/schemas/Error' }
73
- }
74
- }
75
- }
76
- }
77
- },
78
- security: [{ bearerAuth: [] }]
79
- };
80
-
81
- // Add error schema
82
- spec.components.schemas.Error = {
83
- type: 'object',
84
- properties: {
85
- errors: {
86
- type: 'array',
87
- items: {
88
- type: 'object',
89
- properties: {
90
- status: { type: 'string' },
91
- title: { type: 'string' },
92
- detail: { type: 'string' },
93
- source: {
94
- type: 'object',
95
- properties: {
96
- pointer: { type: 'string' }
97
- }
98
- }
99
- }
100
- }
101
- }
102
- }
103
- };
104
-
105
- // Generate schemas from registered schemas
106
- for (const [type, schema] of api.schemas) {
107
- const jsonSchema = schemaToJsonSchema(schema);
108
-
109
- // Resource schema
110
- spec.components.schemas[`${type}Resource`] = {
111
- type: 'object',
112
- properties: {
113
- id: { type: 'string' },
114
- type: { type: 'string', enum: [type] },
115
- attributes: jsonSchema
116
- },
117
- required: ['type', 'attributes']
118
- };
119
-
120
- // Collection schema
121
- spec.components.schemas[`${type}Collection`] = {
122
- type: 'object',
123
- properties: {
124
- data: {
125
- type: 'array',
126
- items: { $ref: `#/components/schemas/${type}Resource` }
127
- },
128
- meta: {
129
- type: 'object',
130
- properties: {
131
- total: { type: 'integer' },
132
- pageSize: { type: 'integer' },
133
- pageNumber: { type: 'integer' },
134
- totalPages: { type: 'integer' }
135
- }
136
- },
137
- links: {
138
- type: 'object',
139
- properties: {
140
- self: { type: 'string' },
141
- first: { type: 'string' },
142
- last: { type: 'string' },
143
- prev: { type: 'string' },
144
- next: { type: 'string' }
145
- }
146
- }
147
- }
148
- };
149
-
150
- // Single resource response
151
- spec.components.schemas[`${type}Response`] = {
152
- type: 'object',
153
- properties: {
154
- data: { $ref: `#/components/schemas/${type}Resource` }
155
- }
156
- };
157
-
158
- // Generate paths
159
- spec.paths[`/${type}`] = {
160
- get: {
161
- tags: [type],
162
- summary: `List ${type}`,
163
- operationId: `list${capitalize(type)}`,
164
- parameters: [
165
- {
166
- name: 'filter',
167
- in: 'query',
168
- style: 'deepObject',
169
- explode: true,
170
- schema: { type: 'object' },
171
- description: 'Filter resources'
172
- },
173
- {
174
- name: 'sort',
175
- in: 'query',
176
- schema: { type: 'string' },
177
- description: 'Sort resources (use - for descending)'
178
- },
179
- {
180
- name: 'page[size]',
181
- in: 'query',
182
- schema: { type: 'integer', default: 10 },
183
- description: 'Page size'
184
- },
185
- {
186
- name: 'page[number]',
187
- in: 'query',
188
- schema: { type: 'integer', default: 1 },
189
- description: 'Page number'
190
- }
191
- ],
192
- responses: {
193
- 200: {
194
- description: 'Success',
195
- content: {
196
- 'application/vnd.api+json': {
197
- schema: { $ref: `#/components/schemas/${type}Collection` }
198
- }
199
- }
200
- }
201
- }
202
- },
203
- post: {
204
- tags: [type],
205
- summary: `Create ${type}`,
206
- operationId: `create${capitalize(type)}`,
207
- requestBody: {
208
- required: true,
209
- content: {
210
- 'application/vnd.api+json': {
211
- schema: {
212
- type: 'object',
213
- properties: {
214
- data: {
215
- type: 'object',
216
- properties: {
217
- type: { type: 'string', enum: [type] },
218
- attributes: jsonSchema
219
- }
220
- }
221
- }
222
- }
223
- }
224
- }
225
- },
226
- responses: {
227
- 201: {
228
- description: 'Created',
229
- content: {
230
- 'application/vnd.api+json': {
231
- schema: { $ref: `#/components/schemas/${type}Response` }
232
- }
233
- }
234
- },
235
- 422: { $ref: '#/components/responses/ValidationError' }
236
- }
237
- }
238
- };
239
-
240
- spec.paths[`/${type}/{id}`] = {
241
- parameters: [{ $ref: '#/components/parameters/idParam' }],
242
- get: {
243
- tags: [type],
244
- summary: `Get ${type}`,
245
- operationId: `get${capitalize(type)}`,
246
- responses: {
247
- 200: {
248
- description: 'Success',
249
- content: {
250
- 'application/vnd.api+json': {
251
- schema: { $ref: `#/components/schemas/${type}Response` }
252
- }
253
- }
254
- },
255
- 404: { $ref: '#/components/responses/NotFound' }
256
- }
257
- },
258
- patch: {
259
- tags: [type],
260
- summary: `Update ${type}`,
261
- operationId: `update${capitalize(type)}`,
262
- requestBody: {
263
- required: true,
264
- content: {
265
- 'application/vnd.api+json': {
266
- schema: {
267
- type: 'object',
268
- properties: {
269
- data: {
270
- type: 'object',
271
- properties: {
272
- id: { type: 'string' },
273
- type: { type: 'string', enum: [type] },
274
- attributes: jsonSchema
275
- }
276
- }
277
- }
278
- }
279
- }
280
- }
281
- },
282
- responses: {
283
- 200: {
284
- description: 'Success',
285
- content: {
286
- 'application/vnd.api+json': {
287
- schema: { $ref: `#/components/schemas/${type}Response` }
288
- }
289
- }
290
- },
291
- 404: { $ref: '#/components/responses/NotFound' },
292
- 422: { $ref: '#/components/responses/ValidationError' }
293
- }
294
- },
295
- delete: {
296
- tags: [type],
297
- summary: `Delete ${type}`,
298
- operationId: `delete${capitalize(type)}`,
299
- responses: {
300
- 204: { description: 'No Content' },
301
- 404: { $ref: '#/components/responses/NotFound' }
302
- }
303
- }
304
- };
305
- }
306
-
307
- return spec;
308
- };
309
-
310
- // Serve OpenAPI spec
311
- if (api.router) {
312
- api.router.get('/openapi.json', (req, res) => {
313
- res.json(api.generateOpenAPISpec());
314
- });
315
-
316
- // Serve Swagger UI
317
- api.router.get('/docs', (req, res) => {
318
- res.send(`
319
- <!DOCTYPE html>
320
- <html>
321
- <head>
322
- <title>${defaultOptions.title}</title>
323
- <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
324
- </head>
325
- <body>
326
- <div id="swagger-ui"></div>
327
- <script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
328
- <script>
329
- SwaggerUIBundle({
330
- url: './openapi.json',
331
- dom_id: '#swagger-ui',
332
- deepLinking: true,
333
- presets: [
334
- SwaggerUIBundle.presets.apis,
335
- SwaggerUIBundle.SwaggerUIStandalonePreset
336
- ]
337
- });
338
- </script>
339
- </body>
340
- </html>
341
- `);
342
- });
343
- }
344
- }
345
- };
346
-
347
- // Convert Schema to JSON Schema
348
- function schemaToJsonSchema(schema) {
349
- const jsonSchema = {
350
- type: 'object',
351
- properties: {},
352
- required: []
353
- };
354
-
355
- for (const [field, definition] of Object.entries(schema.structure)) {
356
- const prop = { type: getJsonType(definition.type) };
357
-
358
- // Add constraints
359
- if (definition.min !== undefined) prop.minimum = definition.min;
360
- if (definition.max !== undefined) prop.maximum = definition.max;
361
- if (definition.enum) prop.enum = definition.enum;
362
- if (definition.pattern) prop.pattern = definition.pattern;
363
- if (definition.default !== undefined) prop.default = definition.default;
364
- if (definition.description) prop.description = definition.description;
365
-
366
- // String constraints
367
- if (definition.type === 'string') {
368
- if (definition.min) prop.minLength = definition.min;
369
- if (definition.max) prop.maxLength = definition.max;
370
- }
371
-
372
- jsonSchema.properties[field] = prop;
373
-
374
- if (definition.required) {
375
- jsonSchema.required.push(field);
376
- }
377
- }
378
-
379
- return jsonSchema;
380
- }
381
-
382
- function getJsonType(schemaType) {
383
- const typeMap = {
384
- string: 'string',
385
- number: 'number',
386
- boolean: 'boolean',
387
- id: 'string',
388
- timestamp: 'integer',
389
- date: 'string',
390
- dateTime: 'string',
391
- array: 'array',
392
- object: 'object',
393
- serialize: 'string',
394
- blob: 'string'
395
- };
396
- return typeMap[schemaType] || 'string';
397
- }
398
-
399
- function capitalize(str) {
400
- return str.charAt(0).toUpperCase() + str.slice(1);
401
- }
@@ -1,220 +0,0 @@
1
- /**
2
- * Positioning plugin for managing record order
3
- */
4
- export const PositioningPlugin = {
5
- install(api, options = {}) {
6
- const defaultOptions = {
7
- positionField: 'position',
8
- beforeIdField: 'beforeId',
9
- positionFilters: [],
10
- ...options
11
- };
12
-
13
- // Add positioning options to API
14
- api.positioningOptions = defaultOptions;
15
-
16
- // Hook into insert operations
17
- api.hook('beforeInsert', async (context) => {
18
- const { data, options } = context;
19
- const posOptions = { ...defaultOptions, ...options.positioning };
20
-
21
- if (!posOptions.enabled) return;
22
-
23
- // Handle beforeId positioning
24
- if (data[posOptions.beforeIdField] !== undefined) {
25
- context.positioningData = {
26
- beforeId: data[posOptions.beforeIdField],
27
- positionField: posOptions.positionField,
28
- filters: posOptions.positionFilters
29
- };
30
-
31
- // Remove virtual field from data
32
- delete data[posOptions.beforeIdField];
33
- }
34
- });
35
-
36
- // Hook into update operations
37
- api.hook('beforeUpdate', async (context) => {
38
- const { data, options } = context;
39
- const posOptions = { ...defaultOptions, ...options.positioning };
40
-
41
- if (!posOptions.enabled) return;
42
-
43
- // Handle beforeId positioning
44
- if (data[posOptions.beforeIdField] !== undefined) {
45
- context.positioningData = {
46
- beforeId: data[posOptions.beforeIdField],
47
- positionField: posOptions.positionField,
48
- filters: posOptions.positionFilters
49
- };
50
-
51
- // Remove virtual field from data
52
- delete data[posOptions.beforeIdField];
53
- }
54
- });
55
-
56
- // Calculate and apply positioning after insert
57
- api.hook('afterInsert', async (context) => {
58
- if (!context.positioningData) return;
59
-
60
- await applyPositioning(api, context, 'insert');
61
- });
62
-
63
- // Calculate and apply positioning after update
64
- api.hook('afterUpdate', async (context) => {
65
- if (!context.positioningData) return;
66
-
67
- await applyPositioning(api, context, 'update');
68
- });
69
-
70
- // Add positioning helpers to API
71
- api.reposition = async (type, recordId, beforeId, options = {}) => {
72
- const posOptions = { ...defaultOptions, ...options };
73
-
74
- // Update the record with new position
75
- await api.update(recordId, {
76
- [posOptions.beforeIdField]: beforeId
77
- }, {
78
- type,
79
- positioning: { enabled: true, ...posOptions }
80
- });
81
- };
82
-
83
- // Get next position in sequence
84
- api.getNextPosition = async (type, filters = {}, options = {}) => {
85
- const posOptions = { ...defaultOptions, ...options };
86
-
87
- // Query to find max position
88
- const result = await api.query({
89
- filter: filters,
90
- sort: `-${posOptions.positionField}`,
91
- page: { size: 1 }
92
- }, { type });
93
-
94
- if (result.data.length > 0) {
95
- const maxPosition = result.data[0].attributes[posOptions.positionField] || 0;
96
- return maxPosition + 1;
97
- }
98
-
99
- return 1;
100
- };
101
-
102
- // Normalize positions (remove gaps)
103
- api.normalizePositions = async (type, filters = {}, options = {}) => {
104
- const posOptions = { ...defaultOptions, ...options };
105
-
106
- // Get all records sorted by position
107
- const result = await api.query({
108
- filter: filters,
109
- sort: posOptions.positionField,
110
- page: { size: 10000 } // Get all records
111
- }, { type });
112
-
113
- // Update positions to remove gaps
114
- let position = 1;
115
- for (const record of result.data) {
116
- if (record.attributes[posOptions.positionField] !== position) {
117
- await api.update(record.id, {
118
- [posOptions.positionField]: position
119
- }, { type });
120
- }
121
- position++;
122
- }
123
- };
124
- }
125
- };
126
-
127
- /**
128
- * Apply positioning logic
129
- */
130
- async function applyPositioning(api, context, operation) {
131
- const { positioningData, result, options } = context;
132
- const { beforeId, positionField, filters } = positioningData;
133
-
134
- // Build filter for position queries
135
- const positionFilter = { ...filters };
136
-
137
- // Handle different beforeId values
138
- if (beforeId === null) {
139
- // Place at end - get max position
140
- const maxPosResult = await api.query({
141
- filter: positionFilter,
142
- sort: `-${positionField}`,
143
- page: { size: 1 }
144
- }, options);
145
-
146
- const maxPosition = maxPosResult.data.length > 0
147
- ? (maxPosResult.data[0].attributes[positionField] || 0)
148
- : 0;
149
-
150
- const newPosition = maxPosition + 1;
151
-
152
- // Update the record with new position
153
- await updatePosition(api, context, newPosition);
154
-
155
- } else if (beforeId) {
156
- // Place before specific record
157
- const beforeRecord = await api.get(beforeId, options);
158
-
159
- if (beforeRecord.data) {
160
- const targetPosition = beforeRecord.data.attributes[positionField] || 1;
161
-
162
- // Shift positions of records at or after target position
163
- await shiftPositions(api, context, targetPosition, positionFilter);
164
-
165
- // Update the record with target position
166
- await updatePosition(api, context, targetPosition);
167
- }
168
- }
169
- // If beforeId is undefined, keep existing position (do nothing)
170
- }
171
-
172
- /**
173
- * Shift positions to make room
174
- */
175
- async function shiftPositions(api, context, fromPosition, filter) {
176
- const { options } = context;
177
- const { positionField } = context.positioningData;
178
-
179
- // Get all records that need shifting
180
- const result = await api.query({
181
- filter: {
182
- ...filter,
183
- [`${positionField}[gte]`]: fromPosition
184
- },
185
- sort: `-${positionField}`, // Sort descending to avoid conflicts
186
- page: { size: 10000 }
187
- }, options);
188
-
189
- // Shift each record
190
- for (const record of result.data) {
191
- const currentPos = record.attributes[positionField];
192
- await api.update(record.id, {
193
- [positionField]: currentPos + 1
194
- }, {
195
- ...options,
196
- positioning: { enabled: false } // Disable positioning to avoid recursion
197
- });
198
- }
199
- }
200
-
201
- /**
202
- * Update record position
203
- */
204
- async function updatePosition(api, context, position) {
205
- const { result, options } = context;
206
- const { positionField } = context.positioningData;
207
-
208
- // Update the record with new position
209
- const updateResult = await api.update(result.data.id, {
210
- [positionField]: position
211
- }, {
212
- ...options,
213
- positioning: { enabled: false } // Disable positioning to avoid recursion
214
- });
215
-
216
- // Update the result in context
217
- if (updateResult.data) {
218
- context.result = updateResult;
219
- }
220
- }