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.
- package/README.md +24 -2
- package/docs/API.md +157 -33
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
- package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
- package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
- package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
- package/docs/GUIDE/index.md +4 -1
- package/fixing_plan.md +112 -0
- package/index.js +2 -0
- package/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +31 -38
- package/plugins/core/connectors/express-plugin.js +6 -7
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- package/plugins/core/connectors/lib/connector-core.js +17 -11
- package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
- package/plugins/core/file-handling-plugin.js +126 -96
- package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
- package/plugins/core/lib/anyapi/schema-utils.js +44 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
- package/plugins/core/lib/dbIntrospection.js +1 -1
- package/plugins/core/lib/dbTablesOperations.js +10 -3
- package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -16
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
- package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
- package/plugins/core/lib/querying/knex-window-queries.js +10 -77
- package/plugins/core/lib/querying/url-helpers.js +9 -21
- package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
- package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -3
- package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
- package/plugins/core/lib/storage/storage-adapter.js +76 -14
- package/plugins/core/lib/storage/storage-mapping.js +86 -16
- package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
- package/plugins/core/rest-api-knex-plugin.js +93 -76
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
- package/plugins/core/rest-api-plugin-methods/common.js +112 -72
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
- package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
- package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
- package/plugins/core/rest-api-plugin-methods/get.js +7 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
- package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
- package/plugins/core/rest-api-plugin-methods/post.js +12 -7
- package/plugins/core/rest-api-plugin-methods/put.js +35 -10
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +11 -37
- package/plugins/core/rest-api-positioning-plugin.js +1 -4
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/core/socketio-plugin.js +0 -1
- package/plugins/storage/local-storage.js +45 -20
- package/plugins/storage/s3-storage.js +23 -54
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +8 -1
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
- package/tests/db-introspection.test.js +4 -1
- package/tests/db-table-operations.test.js +25 -20
- package/tests/fastify-plugin.test.js +49 -6
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +495 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/id-normalization.test.js +444 -0
- package/tests/local-storage.test.js +96 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +132 -11
- package/tests/pagination-enhanced.test.js +66 -15
- package/tests/post-commit-hooks.test.js +120 -0
- package/tests/public-exports.test.js +15 -0
- package/tests/put-create-parity.test.js +216 -0
- package/tests/relationship-endpoints.test.js +259 -0
- package/tests/return-record-settings.test.js +184 -0
- package/tests/row-policy.test.js +433 -0
- package/tests/s3-storage.test.js +38 -0
- package/tests/searchschema-merge.test.js +17 -0
- package/tests/socketio.test.js +62 -0
- package/tests/storage-mapping.test.js +56 -6
- 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
|
|
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 |
|
|
118
|
+
| **Best For** | Local file storage | Tests, examples, S3-style URL demos |
|
|
119
119
|
|
|
120
|
-
### S3Storage
|
|
120
|
+
### S3Storage (Mock/Demo)
|
|
121
121
|
|
|
122
|
-
|
|
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:
|
|
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,
|
|
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 (
|
|
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:
|
|
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
|
|
540
|
-
return `${
|
|
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
|
|
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
|
|
33
|
-
userId: { type: 'id', required: true
|
|
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:
|