json-rest-api 1.0.23 → 1.0.25

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 (106) hide show
  1. package/README.md +24 -2
  2. package/docs/API.md +157 -33
  3. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
  4. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
  5. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
  6. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
  7. package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
  8. package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
  9. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
  10. package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
  11. package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
  12. package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
  13. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
  14. package/docs/GUIDE/index.md +4 -1
  15. package/fixing_plan.md +112 -0
  16. package/index.js +2 -0
  17. package/lib/enhanced-logger.js +1 -1
  18. package/package.json +6 -2
  19. package/plugins/core/bulk-operations-plugin.js +31 -38
  20. package/plugins/core/connectors/express-plugin.js +6 -7
  21. package/plugins/core/connectors/fastify-plugin.js +5 -1
  22. package/plugins/core/connectors/lib/busboy-detector.js +4 -4
  23. package/plugins/core/connectors/lib/connector-core.js +17 -11
  24. package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
  25. package/plugins/core/file-handling-plugin.js +126 -96
  26. package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
  27. package/plugins/core/lib/anyapi/schema-utils.js +44 -4
  28. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
  29. package/plugins/core/lib/dbIntrospection.js +1 -1
  30. package/plugins/core/lib/dbTablesOperations.js +10 -3
  31. package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
  32. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -16
  33. package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
  34. package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
  35. package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
  36. package/plugins/core/lib/querying/knex-window-queries.js +10 -77
  37. package/plugins/core/lib/querying/url-helpers.js +9 -21
  38. package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
  39. package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
  40. package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
  41. package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
  42. package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -3
  43. package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
  44. package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
  45. package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
  46. package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
  47. package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
  48. package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
  49. package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
  50. package/plugins/core/lib/storage/storage-adapter.js +76 -14
  51. package/plugins/core/lib/storage/storage-mapping.js +86 -16
  52. package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
  53. package/plugins/core/lib/writing/relationship-processor.js +10 -1
  54. package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
  55. package/plugins/core/rest-api-knex-plugin.js +93 -76
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
  57. package/plugins/core/rest-api-plugin-methods/common.js +112 -72
  58. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
  59. package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
  60. package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
  61. package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
  62. package/plugins/core/rest-api-plugin-methods/get.js +7 -1
  63. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
  64. package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
  65. package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
  66. package/plugins/core/rest-api-plugin-methods/post.js +12 -7
  67. package/plugins/core/rest-api-plugin-methods/put.js +35 -10
  68. package/plugins/core/rest-api-plugin-methods/release.js +0 -2
  69. package/plugins/core/rest-api-plugin.js +11 -37
  70. package/plugins/core/rest-api-positioning-plugin.js +1 -4
  71. package/plugins/core/rest-api-row-policy-plugin.js +157 -0
  72. package/plugins/core/socketio-plugin.js +0 -1
  73. package/plugins/storage/local-storage.js +45 -20
  74. package/plugins/storage/s3-storage.js +23 -54
  75. package/quickTest.js +1 -0
  76. package/scripts/run-docs.js +80 -84
  77. package/test-debug.js +1 -1
  78. package/tests/README.md +8 -1
  79. package/tests/anyapi-basic.test.js +3 -0
  80. package/tests/anyapi-cursor-pagination.test.js +7 -1
  81. package/tests/anyapi-custom-idproperty.test.js +163 -0
  82. package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
  83. package/tests/db-introspection.test.js +4 -1
  84. package/tests/db-table-operations.test.js +25 -20
  85. package/tests/fastify-plugin.test.js +49 -6
  86. package/tests/field-setters.test.js +10 -10
  87. package/tests/file-handling.test.js +118 -0
  88. package/tests/fixtures/api-configs.js +495 -4
  89. package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
  90. package/tests/http-connectors-parity.test.js +165 -5
  91. package/tests/id-normalization.test.js +444 -0
  92. package/tests/local-storage.test.js +96 -0
  93. package/tests/logical-id-write-validation.test.js +151 -0
  94. package/tests/pagination-cursor-multifield.test.js +132 -11
  95. package/tests/pagination-enhanced.test.js +66 -15
  96. package/tests/post-commit-hooks.test.js +120 -0
  97. package/tests/public-exports.test.js +15 -0
  98. package/tests/put-create-parity.test.js +216 -0
  99. package/tests/relationship-endpoints.test.js +259 -0
  100. package/tests/return-record-settings.test.js +184 -0
  101. package/tests/row-policy.test.js +433 -0
  102. package/tests/s3-storage.test.js +38 -0
  103. package/tests/searchschema-merge.test.js +17 -0
  104. package/tests/socketio.test.js +62 -0
  105. package/tests/storage-mapping.test.js +56 -6
  106. package/tests/test-development-guide.md +5 -5
@@ -77,6 +77,69 @@ await api.use(AutoFilterPlugin, {
77
77
  })
78
78
  ```
79
79
 
80
+ ## Storage Backends
81
+
82
+ `AutoFilterPlugin` works with either database storage plugin:
83
+
84
+ - `RestApiKnexPlugin` for resources backed by normal database tables
85
+ - `RestApiAnyapiKnexPlugin` for logical resources backed by AnyAPI canonical storage
86
+
87
+ An `Api` instance has one storage engine. Install either storage plugin, then install `AutoFilterPlugin`. If an application keeps core resources in normal tables and user-defined resources in AnyAPI, create one `Api` instance for each storage engine. Each instance owns its resources and autofilter configuration.
88
+
89
+ Autofilter definitions always use logical schema field names. The selected storage plugin translates those fields to normal table columns or AnyAPI canonical slots.
90
+
91
+ ### Workspace Scoping with AnyAPI
92
+
93
+ AnyAPI's `tenantId` is fixed when `RestApiAnyapiKnexPlugin` is installed. It identifies the metadata and canonical-storage namespace; it is not a per-request workspace value. For one AnyAPI instance serving multiple application workspaces, use a stable internal `tenantId` and persist `workspace_id` on every workspace-scoped resource:
94
+
95
+ ```js
96
+ import { Api } from 'hooked-api'
97
+ import {
98
+ RestApiPlugin,
99
+ RestApiAnyapiKnexPlugin,
100
+ AutoFilterPlugin
101
+ } from 'json-rest-api'
102
+
103
+ const api = new Api({ name: 'form-data-api' })
104
+
105
+ await api.use(RestApiPlugin)
106
+ await api.use(RestApiAnyapiKnexPlugin, {
107
+ knex,
108
+ tenantId: 'application'
109
+ })
110
+
111
+ await api.use(AutoFilterPlugin, {
112
+ resolvers: {
113
+ workspace: ({ context }) => context.session?.workspaceId
114
+ },
115
+ presets: {
116
+ workspace: {
117
+ filters: [
118
+ { field: 'workspace_id', resolver: 'workspace' }
119
+ ]
120
+ }
121
+ }
122
+ })
123
+
124
+ await api.addResource('form_123', {
125
+ schema: {
126
+ id: { type: 'id' },
127
+ workspace_id: { type: 'string', required: true },
128
+ customer_name: { type: 'string', required: true },
129
+ subscribed: { type: 'boolean' }
130
+ },
131
+ autofilter: 'workspace'
132
+ })
133
+ ```
134
+
135
+ In this arrangement:
136
+
137
+ - `tenantId: 'application'` selects one fixed AnyAPI metadata and canonical-storage namespace. Do not derive it from request context.
138
+ - `workspace_id` isolates application workspaces. `AutoFilterPlugin` stamps it on writes and filters by it on reads.
139
+ - A trusted resolver supplies the current workspace. Do not resolve it directly from an unverified request field.
140
+ - User-defined forms with different schemas need distinct resource names within the AnyAPI namespace, such as `form_123` and `form_456`. Put the workspace autofilter on each resource.
141
+ - Workspaces may use the same logical resource when they share its schema; `workspace_id` still keeps their records separate.
142
+
80
143
  ## Resource Configuration
81
144
 
82
145
  Apply a preset by name:
@@ -19,7 +19,7 @@ The file handling system consists of three main components:
19
19
 
20
20
  1. **FileHandlingPlugin** - Orchestrates file detection and processing
21
21
  2. **Protocol Detectors** - Parse files from different protocols (HTTP, Express)
22
- 3. **Storage Adapters** - Save files to different backends (local, S3, etc.)
22
+ 3. **Storage Adapters** - Save files to local storage or generate mock S3-style URLs for demos
23
23
 
24
24
  ### How It Works
25
25
 
@@ -115,11 +115,11 @@ Storage adapters handle where and how files are saved. The library includes two
115
115
  | **Extension Whitelist** | ✅ Yes | ❌ No |
116
116
  | **Duplicate Handling** | ✅ Yes | ✅ Automatic |
117
117
  | **Custom Naming** | ✅ Yes | ❌ No |
118
- | **Best For** | Local file storage | Cloud storage |
118
+ | **Best For** | Local file storage | Tests, examples, S3-style URL demos |
119
119
 
120
- ### S3Storage
120
+ ### S3Storage (Mock/Demo)
121
121
 
122
- Saves files to Amazon S3 or S3-compatible storage:
122
+ Generates S3-style URLs without uploading files to Amazon S3:
123
123
 
124
124
  ```javascript
125
125
  import { S3Storage } from 'json-rest-api/plugins/storage/s3-storage.js';
@@ -129,7 +129,7 @@ const s3Storage = new S3Storage({
129
129
  region: 'us-east-1', // AWS region (default: 'us-east-1')
130
130
  prefix: 'uploads/', // Path prefix in bucket (default: '')
131
131
  acl: 'public-read', // Access control (default: 'public-read')
132
- mockMode: false // Use mock mode? (default: true)
132
+ mockMode: true // Only supported mode
133
133
  });
134
134
  ```
135
135
 
@@ -137,7 +137,7 @@ const s3Storage = new S3Storage({
137
137
  - Always generates random hash + extension (e.g., `uploads/a7f8d9e2b4c6e1f3.jpg`)
138
138
  - Original filenames are never used for security
139
139
 
140
- **Note**: The included S3Storage is a mock implementation for demonstration. For production use, you'll need to implement the actual AWS SDK calls.
140
+ **Note**: The included S3Storage is a mock implementation for demonstration. `mockMode: false` throws a clear error because real S3 uploads are not implemented. For production use, provide your own storage adapter or add an AWS SDK-backed implementation.
141
141
 
142
142
  ### LocalStorage
143
143
 
@@ -187,9 +187,11 @@ const localStorage = new LocalStorage({
187
187
  }
188
188
  // Result: "user_12345_1672531200000.jpg"
189
189
  ```
190
+ Custom generators must return a basename only, not a path. Values containing `/`, `\`, drive prefixes, or only whitespace are rejected.
190
191
 
191
192
  **Security Features:**
192
- - Path traversal protection (removes `..` and `/`)
193
+ - Path traversal protection (sanitizes original names and rejects path-like custom names)
194
+ - Custom generated names are restricted to basenames
193
195
  - Control character filtering
194
196
  - Extension validation against whitelist
195
197
  - Automatic duplicate handling
@@ -218,12 +220,15 @@ class MyCustomStorage {
218
220
  }
219
221
 
220
222
  async delete(url) {
221
- // Optional: implement file deletion
223
+ // Optional but recommended: FileHandlingPlugin calls this after
224
+ // an uploaded file's write transaction rolls back.
222
225
  await deleteFileSomewhere(url);
223
226
  }
224
227
  }
225
228
  ```
226
229
 
230
+ `FileHandlingPlugin` always calls detector-provided `file.cleanup()` after parsing, including validation failures. If a file has already been uploaded and the later resource write rolls back, the plugin calls `storage.delete(url)` when the adapter implements it. Storage adapters that need rollback cleanup should make `delete(url)` idempotent for URLs returned by `upload(file)`.
231
+
227
232
  ## Protocol Configuration
228
233
 
229
234
  Different protocols have different configuration options for file parsing.
@@ -536,11 +541,11 @@ const organizedStorage = new LocalStorage({
536
541
  nameStrategy: 'custom',
537
542
  nameGenerator: async (file) => {
538
543
  const date = new Date();
539
- const dateDir = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}`;
540
- return `${dateDir}/${crypto.randomBytes(16).toString('hex')}`;
544
+ const datePrefix = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
545
+ return `${datePrefix}-${crypto.randomBytes(16).toString('hex')}`;
541
546
  }
542
547
  });
543
- // Saves as: "2024/01/a7f8d9e2b4c6e1f3.jpg"
548
+ // Saves as: "2024-01-a7f8d9e2b4c6e1f3.jpg"
544
549
  ```
545
550
 
546
551
  ### Using cURL
@@ -721,4 +726,4 @@ Then visit http://localhost:3000 to see the test forms.
721
726
  - Add image processing (thumbnails, resizing)
722
727
  - Implement virus scanning for uploads
723
728
  - Add progress tracking for large files
724
- - Create a chunked upload system for very large files
729
+ - Create a chunked upload system for very large files
@@ -29,8 +29,8 @@ Example:
29
29
  await api.addResource('memberships', {
30
30
  schema: {
31
31
  id: { type: 'id' },
32
- workspaceId: { type: 'id', required: true, storage: { column: 'workspace_id' } },
33
- userId: { type: 'id', required: true, storage: { column: 'user_id' } },
32
+ workspaceId: { type: 'id', required: true },
33
+ userId: { type: 'id', required: true },
34
34
  role: { type: 'string', enum: ['owner', 'member'], required: true, defaultTo: 'member' }
35
35
  },
36
36
 
@@ -64,6 +64,126 @@ await api.addResource('memberships', {
64
64
  })
65
65
  ```
66
66
 
67
+ ## Default Column Naming
68
+
69
+ Table-backed resources keep logical field names in the resource schema and map them to physical columns for Knex operations.
70
+
71
+ By default, physical columns use snake_case:
72
+
73
+ - `workspaceId` -> `workspace_id`
74
+ - `userId` -> `user_id`
75
+ - `createdAt` -> `created_at`
76
+
77
+ This keeps the API surface expressive without repeating `storage.column` on every camelCase field.
78
+
79
+ You only need `storage.column` when a field should use a non-standard column name:
80
+
81
+ ```js
82
+ await api.addResource('profiles', {
83
+ schema: {
84
+ id: { type: 'id' },
85
+ displayName: { type: 'string', required: true },
86
+ legacyRef: { type: 'string', storage: { column: 'legacy_profile_ref' } }
87
+ },
88
+ tableName: 'profiles'
89
+ })
90
+ ```
91
+
92
+ If you need physical columns to match the logical field names exactly for a whole resource, opt out explicitly:
93
+
94
+ ```js
95
+ await api.addResource('verbatim_profiles', {
96
+ storage: { naming: 'exact' },
97
+ schema: {
98
+ id: { type: 'id' },
99
+ displayName: { type: 'string', required: true }
100
+ },
101
+ tableName: 'verbatim_profiles'
102
+ })
103
+ ```
104
+
105
+ ## Logical IDs and `idProperty`
106
+
107
+ `idProperty` names the physical primary-key column for table-backed resources. The API contract still uses the logical resource id.
108
+
109
+ ```js
110
+ await api.addResource('profiles', {
111
+ idProperty: 'user_id',
112
+ schema: {
113
+ id: { type: 'id', required: true, storage: { column: 'user_id' } },
114
+ displayName: { type: 'string', required: true },
115
+ loginCount: { type: 'number', defaultTo: 0 }
116
+ },
117
+ tableName: 'profiles'
118
+ })
119
+ ```
120
+
121
+ With this definition:
122
+
123
+ - writes send the resource id as `id` or `data.id`
124
+ - reads return the resource id as `id` or `data.id`
125
+ - the primary key is stored in `user_id`
126
+ - `displayName` and `loginCount` are stored in `display_name` and `login_count`
127
+
128
+ The resource id is not part of `attributes`:
129
+
130
+ ```js
131
+ await api.resources.profiles.post({
132
+ inputRecord: {
133
+ data: {
134
+ type: 'profiles',
135
+ id: '42',
136
+ attributes: {
137
+ displayName: 'Mercury'
138
+ }
139
+ }
140
+ }
141
+ })
142
+ ```
143
+
144
+ ### ID normalization
145
+
146
+ If your primary keys need canonicalization beyond the default trim-and-stringify behavior, configure `normalizeId`.
147
+
148
+ ```js
149
+ await api.use(RestApiPlugin, {
150
+ normalizeId: (value) => {
151
+ if (value === null || value === undefined) return null
152
+
153
+ const normalized = String(value).trim()
154
+ return normalized ? normalized.toUpperCase() : null
155
+ }
156
+ })
157
+ ```
158
+
159
+ You can override that per resource:
160
+
161
+ ```js
162
+ await api.addResource('profiles', {
163
+ idProperty: 'user_id',
164
+ normalizeId: (value) => {
165
+ if (value === null || value === undefined) return null
166
+
167
+ const normalized = String(value).trim()
168
+ return normalized ? normalized.toLowerCase() : null
169
+ },
170
+ schema: {
171
+ id: { type: 'id', required: true, storage: { column: 'user_id' } },
172
+ displayName: { type: 'string', required: true }
173
+ },
174
+ tableName: 'profiles'
175
+ })
176
+ ```
177
+
178
+ For table-backed resources, `normalizeId` is applied before:
179
+
180
+ - reading a record by id
181
+ - replacing, patching, or deleting a record
182
+ - persisting an explicit `POST` resource id
183
+ - validating or writing relationship identifiers that point at the resource
184
+
185
+ If the normalizer returns an empty value, existing-resource operations fail as `not_found`, while explicit `POST` document ids fail validation on `data.id`.
186
+
67
187
  ## Create Tables
68
188
 
69
189
  Create the table directly from the resource definition: