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
package/schema.js DELETED
@@ -1,481 +0,0 @@
1
- import { stringify as serialize, parse as deserialize } from 'circular-json-es6';
2
-
3
- /**
4
- * Schema class for validating and transforming data with plugin support
5
- */
6
- export class Schema {
7
- constructor(structure = {}, options = {}) {
8
- this.structure = structure;
9
- this.options = options;
10
- this.plugins = [];
11
- this.types = new Map();
12
- this.params = new Map();
13
- this._initializeBuiltins();
14
- }
15
-
16
- /**
17
- * Add a plugin to the schema
18
- */
19
- use(plugin, options = {}) {
20
- if (typeof plugin.install === 'function') {
21
- plugin.install(this, options);
22
- }
23
- this.plugins.push({ plugin, options });
24
- return this;
25
- }
26
-
27
- /**
28
- * Register a custom type handler
29
- */
30
- registerType(name, handler) {
31
- this.types.set(name, handler);
32
- return this;
33
- }
34
-
35
- /**
36
- * Register a custom parameter handler
37
- */
38
- registerParam(name, handler) {
39
- this.params.set(name, handler);
40
- return this;
41
- }
42
-
43
- /**
44
- * Initialize built-in types and parameters
45
- */
46
- _initializeBuiltins() {
47
- // Built-in types
48
- this.registerType('none', this._noneType);
49
- this.registerType('string', this._stringType);
50
- this.registerType('blob', this._blobType);
51
- this.registerType('number', this._numberType);
52
- this.registerType('timestamp', this._timestampType);
53
- this.registerType('dateTime', this._dateTimeType);
54
- this.registerType('date', this._dateType);
55
- this.registerType('array', this._arrayType);
56
- this.registerType('object', this._objectType);
57
- this.registerType('serialize', this._serializeType);
58
- this.registerType('boolean', this._booleanType);
59
- this.registerType('id', this._idType);
60
-
61
- // Built-in parameters
62
- this.registerParam('required', this._requiredParam);
63
- this.registerParam('min', this._minParam);
64
- this.registerParam('max', this._maxParam);
65
- this.registerParam('validator', this._validatorParam);
66
- this.registerParam('uppercase', this._uppercaseParam);
67
- this.registerParam('lowercase', this._lowercaseParam);
68
- this.registerParam('trim', this._trimParam);
69
- this.registerParam('length', this._lengthParam);
70
- this.registerParam('default', this._defaultParam);
71
- this.registerParam('notEmpty', this._notEmptyParam);
72
- }
73
-
74
- /**
75
- * Validate an object against the schema
76
- */
77
- async validate(object, options = {}) {
78
- const errors = [];
79
- const validatedObject = { ...object };
80
-
81
- // Check for unknown fields unless we're only validating object values
82
- if (!options.onlyObjectValues) {
83
- for (const fieldName in object) {
84
- if (!this.structure[fieldName]) {
85
- errors.push({
86
- field: fieldName,
87
- message: 'Field not allowed',
88
- code: 'FIELD_NOT_ALLOWED'
89
- });
90
- }
91
- }
92
- }
93
-
94
- // Determine target object for validation
95
- const targetObject = options.onlyObjectValues ? object : this.structure;
96
-
97
- for (const fieldName in targetObject) {
98
- const definition = this.structure[fieldName];
99
- if (!definition) continue;
100
-
101
- let skipCast = false;
102
- let skipParams = false;
103
-
104
- // Get field options
105
- const canBeNull = this._getCanBeNull(definition, options);
106
- const emptyAsNull = this._getEmptyAsNull(definition, options);
107
-
108
- // Handle required fields
109
- if (definition.required && object[fieldName] === undefined) {
110
- if (!this._skipParam('required', options.skipParams, fieldName)) {
111
- const errorMessage = definition.requiredErrorMessage ||
112
- definition.errorMessage ||
113
- 'Field required';
114
- errors.push({
115
- field: fieldName,
116
- message: errorMessage,
117
- code: 'FIELD_REQUIRED'
118
- });
119
- skipParams = true;
120
- }
121
- }
122
-
123
- // Skip casting if not required and undefined
124
- if (!definition.required && object[fieldName] === undefined) {
125
- skipCast = true;
126
- }
127
-
128
- // Handle empty strings
129
- if (String(object[fieldName]) === '' && emptyAsNull) {
130
- validatedObject[fieldName] = null;
131
- skipParams = true;
132
- }
133
-
134
- // Handle null values
135
- if (object[fieldName] === null) {
136
- if (!canBeNull) {
137
- const errorMessage = definition.nullErrorMessage ||
138
- definition.errorMessage ||
139
- 'Field cannot be null';
140
- errors.push({
141
- field: fieldName,
142
- message: errorMessage,
143
- code: 'FIELD_CANNOT_BE_NULL'
144
- });
145
- }
146
- skipParams = true;
147
- }
148
-
149
- // Type casting
150
- if (!skipCast && !skipParams && this.types.has(definition.type)) {
151
- try {
152
- const typeHandler = this.types.get(definition.type);
153
- const result = await typeHandler.call(this, {
154
- definition,
155
- value: object[fieldName],
156
- fieldName,
157
- object: validatedObject,
158
- objectBeforeCast: object,
159
- valueBeforeCast: object[fieldName],
160
- options,
161
- computedOptions: { canBeNull, emptyAsNull }
162
- });
163
- if (result !== undefined) {
164
- validatedObject[fieldName] = result;
165
- }
166
- } catch (error) {
167
- const errorMessage = definition.errorMessage ||
168
- definition.castErrorMessage ||
169
- error.message ||
170
- 'Type casting failed';
171
- errors.push({
172
- field: fieldName,
173
- message: errorMessage,
174
- code: 'TYPE_ERROR'
175
- });
176
- }
177
- }
178
-
179
- // Parameter validation
180
- if (!skipParams) {
181
- for (const paramName in definition) {
182
- if (paramName === 'type' ||
183
- this._skipParam(paramName, options.skipParams, fieldName)) {
184
- continue;
185
- }
186
-
187
- if (this.params.has(paramName)) {
188
- try {
189
- const paramHandler = this.params.get(paramName);
190
- const result = await paramHandler.call(this, {
191
- definition,
192
- value: validatedObject[fieldName],
193
- fieldName,
194
- object: validatedObject,
195
- objectBeforeCast: object,
196
- valueBeforeCast: object[fieldName],
197
- parameterName: paramName,
198
- parameterValue: definition[paramName],
199
- options,
200
- computedOptions: { canBeNull, emptyAsNull }
201
- });
202
- if (result !== undefined) {
203
- validatedObject[fieldName] = result;
204
- }
205
- } catch (error) {
206
- // Support parameter-specific error messages
207
- const paramErrorKey = `${paramName}ErrorMessage`;
208
- const errorMessage = definition[paramErrorKey] ||
209
- definition.errorMessage ||
210
- error.message ||
211
- 'Parameter validation failed';
212
- errors.push({
213
- field: fieldName,
214
- message: errorMessage,
215
- code: 'PARAM_ERROR'
216
- });
217
- }
218
- }
219
- }
220
- }
221
- }
222
-
223
- return { validatedObject, errors };
224
- }
225
-
226
- /**
227
- * Clean up an object to only include fields with a specific parameter
228
- */
229
- cleanup(object, parameterName) {
230
- const cleanedObject = {};
231
- for (const key in object) {
232
- if (this.structure[key] && this.structure[key][parameterName]) {
233
- cleanedObject[key] = object[key];
234
- }
235
- }
236
- return cleanedObject;
237
- }
238
-
239
- /**
240
- * Helper method to validate with onlyObjectValues option
241
- * Useful for partial updates
242
- */
243
- async validatePartial(object, options = {}) {
244
- return this.validate(object, { ...options, onlyObjectValues: true });
245
- }
246
-
247
- // Built-in type handlers
248
- _noneType({ value }) {
249
- return value;
250
- }
251
-
252
- _stringType({ value, definition }) {
253
- if (value === undefined || value === null) return '';
254
- if (typeof value.toString !== 'function') {
255
- throw new Error('Cannot cast to string');
256
- }
257
- const str = value.toString();
258
- return definition.noTrim ? str : str.trim();
259
- }
260
-
261
- _blobType({ value }) {
262
- return value;
263
- }
264
-
265
- _numberType({ value }) {
266
- if (value === undefined) return 0;
267
- const num = Number(value);
268
- if (isNaN(num)) {
269
- throw new Error('Invalid number');
270
- }
271
- return num;
272
- }
273
-
274
- _timestampType({ value, computedOptions }) {
275
- const num = Number(value);
276
- if (isNaN(num)) {
277
- throw new Error('Invalid timestamp');
278
- }
279
- if (!num && computedOptions.canBeNull) return null;
280
- return num;
281
- }
282
-
283
- _dateTimeType({ value }) {
284
- if (!value) return null;
285
- const date = new Date(value);
286
- if (isNaN(date.getTime())) {
287
- throw new Error('Invalid date');
288
- }
289
- return date.toISOString().slice(0, 19).replace('T', ' ');
290
- }
291
-
292
- _dateType(params) {
293
- const result = this._dateTimeType(params);
294
- return result ? result.slice(0, 10) : result;
295
- }
296
-
297
- _arrayType({ value }) {
298
- return Array.isArray(value) ? value : [value];
299
- }
300
-
301
- _objectType({ value }) {
302
- return value;
303
- }
304
-
305
- _serializeType({ value }) {
306
- try {
307
- return serialize(value);
308
- } catch (error) {
309
- throw new Error('Serialization failed');
310
- }
311
- }
312
-
313
- _booleanType({ value, definition }) {
314
- if (typeof value === 'string') {
315
- const falseValue = definition.stringFalseWhen || 'false';
316
- const trueValue = definition.stringTrueWhen || 'true';
317
- if (value === falseValue) return false;
318
- if (value === trueValue || value === 'on') return true;
319
- return false;
320
- }
321
- return !!value;
322
- }
323
-
324
- _idType({ value }) {
325
- const num = parseInt(value, 10);
326
- if (isNaN(num)) {
327
- throw new Error('Invalid ID');
328
- }
329
- return num;
330
- }
331
-
332
- // Built-in parameter handlers
333
- _requiredParam({ value, fieldName }) {
334
- if (value === undefined || value === null) {
335
- throw new Error(`${fieldName} is required`);
336
- }
337
- }
338
-
339
- _minParam({ value, parameterValue, definition, fieldName }) {
340
- if (value === undefined) return;
341
- if (definition.type === 'number' && value < parameterValue) {
342
- const errorMessage = definition.minErrorMessage ||
343
- definition.errorMessage ||
344
- `${fieldName} is too low`;
345
- throw new Error(errorMessage);
346
- }
347
- if (definition.type === 'string' && value.length < parameterValue) {
348
- const errorMessage = definition.minErrorMessage ||
349
- definition.errorMessage ||
350
- `${fieldName} is too short`;
351
- throw new Error(errorMessage);
352
- }
353
- if (definition.type === 'array' && value.length < parameterValue) {
354
- const errorMessage = definition.minErrorMessage ||
355
- definition.errorMessage ||
356
- `${fieldName} must have at least ${parameterValue} items`;
357
- throw new Error(errorMessage);
358
- }
359
- }
360
-
361
- _maxParam({ value, parameterValue, definition, fieldName }) {
362
- if (value === undefined) return;
363
- if (definition.type === 'number' && value > parameterValue) {
364
- const errorMessage = definition.maxErrorMessage ||
365
- definition.errorMessage ||
366
- `${fieldName} is too high`;
367
- throw new Error(errorMessage);
368
- }
369
- if (definition.type === 'string' && value.length > parameterValue) {
370
- const errorMessage = definition.maxErrorMessage ||
371
- definition.errorMessage ||
372
- `${fieldName} is too long`;
373
- throw new Error(errorMessage);
374
- }
375
- if (definition.type === 'array' && value.length > parameterValue) {
376
- const errorMessage = definition.maxErrorMessage ||
377
- definition.errorMessage ||
378
- `${fieldName} must have at most ${parameterValue} items`;
379
- throw new Error(errorMessage);
380
- }
381
- }
382
-
383
- _validatorParam({ parameterValue, value, object, fieldName, definition }) {
384
- if (typeof parameterValue !== 'function') {
385
- throw new Error('Validator must be a function');
386
- }
387
-
388
- // Support custom validator with custom error message
389
- const validatorResult = parameterValue(value, object, {
390
- schema: this,
391
- fieldName,
392
- definition
393
- });
394
-
395
- // Handle different return types
396
- if (validatorResult === false) {
397
- // Use custom error message if provided
398
- const errorMessage = definition.validatorMessage ||
399
- definition.errorMessage ||
400
- `Validation failed for ${fieldName}`;
401
- throw new Error(errorMessage);
402
- } else if (typeof validatorResult === 'string') {
403
- // Validator returned a custom error message
404
- throw new Error(validatorResult);
405
- } else if (validatorResult instanceof Error) {
406
- throw validatorResult;
407
- }
408
- // If result is true or undefined, validation passed
409
- }
410
-
411
- _uppercaseParam({ value, definition }) {
412
- if (definition.type === 'string' && typeof value === 'string') {
413
- return value.toUpperCase();
414
- }
415
- }
416
-
417
- _lowercaseParam({ value, definition }) {
418
- if (definition.type === 'string' && typeof value === 'string') {
419
- return value.toLowerCase();
420
- }
421
- }
422
-
423
- _trimParam({ value, parameterValue, definition, fieldName, valueBeforeCast }) {
424
- if (definition.type === 'string' && typeof value === 'string') {
425
- return value.slice(0, parameterValue);
426
- }
427
- if (Number.isInteger(Number(valueBeforeCast)) &&
428
- String(Number(valueBeforeCast)).length > parameterValue) {
429
- throw new Error(`${fieldName} out of range`);
430
- }
431
- }
432
-
433
- _lengthParam(params) {
434
- return this._trimParam(params);
435
- }
436
-
437
- _defaultParam({ valueBeforeCast, parameterValue, object, fieldName, definition }) {
438
- if (valueBeforeCast === undefined) {
439
- if (typeof parameterValue === 'function') {
440
- // Pass context to default function for conditional defaults
441
- return parameterValue({
442
- object,
443
- fieldName,
444
- definition,
445
- schema: this
446
- });
447
- }
448
- return parameterValue;
449
- }
450
- }
451
-
452
- _notEmptyParam({ valueBeforeCast, parameterValue, fieldName, definition }) {
453
- const str = valueBeforeCast?.toString ? valueBeforeCast.toString() : '';
454
- if (parameterValue && valueBeforeCast !== undefined && str === '') {
455
- const errorMessage = definition.notEmptyErrorMessage ||
456
- definition.errorMessage ||
457
- `${fieldName} cannot be empty`;
458
- throw new Error(errorMessage);
459
- }
460
- }
461
-
462
- // Helper methods
463
- _skipParam(param, skipParams, field) {
464
- if (!skipParams || typeof skipParams !== 'object') return false;
465
- const fieldSkips = skipParams[field];
466
- return Array.isArray(fieldSkips) && fieldSkips.includes(param);
467
- }
468
-
469
- _getCanBeNull(definition, options) {
470
- if (definition.default === null) return true;
471
- if (typeof definition.canBeNull !== 'undefined') return definition.canBeNull;
472
- if (typeof options.canBeNull !== 'undefined') return !!options.canBeNull;
473
- return false;
474
- }
475
-
476
- _getEmptyAsNull(definition, options) {
477
- if (typeof definition.emptyAsNull !== 'undefined') return definition.emptyAsNull;
478
- if (typeof options.emptyAsNull !== 'undefined') return !!options.emptyAsNull;
479
- return false;
480
- }
481
- }
package/test-basic.js DELETED
@@ -1,107 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { createApi, Schema } from './index.js';
4
-
5
- console.log('Testing basic jsonrestapi functionality...\n');
6
-
7
- // Test 1: Create simple API with memory storage
8
- console.log('1. Creating API with memory storage...');
9
- const api = createApi({
10
- storage: 'memory',
11
- http: {
12
- basePath: '/api/v1'
13
- }
14
- });
15
- console.log('✓ API created successfully');
16
-
17
- // Test 2: Define and register schema
18
- console.log('\n2. Creating and registering user schema...');
19
- const userSchema = new Schema({
20
- id: { type: 'id' },
21
- name: { type: 'string', required: true },
22
- email: { type: 'string', required: true },
23
- age: { type: 'number', min: 0, max: 150 }
24
- });
25
-
26
- // Define hooks for users
27
- const userHooks = {
28
- async transformResult(context) {
29
- // Add a test field to verify hooks work
30
- if (context.result) {
31
- context.result.testField = 'hooks-working';
32
- }
33
- }
34
- };
35
-
36
- api.addResource('users', userSchema, userHooks);
37
- console.log('✓ Schema registered successfully with hooks');
38
-
39
- // Test 3: Insert data
40
- console.log('\n3. Testing insert operation...');
41
- try {
42
- const newUser = await api.insert({
43
- name: 'John Doe',
44
- email: 'john@example.com',
45
- age: 30
46
- }, {
47
- type: 'users'
48
- });
49
-
50
- console.log('✓ User created:', newUser.data);
51
- } catch (error) {
52
- console.error('✗ Insert failed:', error.message);
53
- }
54
-
55
- // Test 4: Query data
56
- console.log('\n4. Testing query operation...');
57
- try {
58
- const result = await api.query({
59
- filter: { age: 30 }
60
- }, {
61
- type: 'users'
62
- });
63
-
64
- console.log('✓ Query successful. Found', result.meta.total, 'users');
65
- } catch (error) {
66
- console.error('✗ Query failed:', error.message);
67
- }
68
-
69
- // Test 5: Get by ID
70
- console.log('\n5. Testing get operation...');
71
- try {
72
- const user = await api.get(1, {
73
- type: 'users'
74
- });
75
-
76
- console.log('✓ User retrieved:', user.data);
77
- } catch (error) {
78
- console.error('✗ Get failed:', error.message);
79
- }
80
-
81
- // Test 6: Update
82
- console.log('\n6. Testing update operation...');
83
- try {
84
- const updated = await api.update(1, {
85
- age: 31
86
- }, {
87
- type: 'users'
88
- });
89
-
90
- console.log('✓ User updated:', updated.data);
91
- } catch (error) {
92
- console.error('✗ Update failed:', error.message);
93
- }
94
-
95
- // Test 7: Delete
96
- console.log('\n7. Testing delete operation...');
97
- try {
98
- await api.delete(1, {
99
- type: 'users'
100
- });
101
-
102
- console.log('✓ User deleted successfully');
103
- } catch (error) {
104
- console.error('✗ Delete failed:', error.message);
105
- }
106
-
107
- console.log('\n✓ All basic tests completed!');