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
|
@@ -118,10 +118,10 @@ describe('dbTablesOperations.createKnexTable', () => {
|
|
|
118
118
|
|
|
119
119
|
await db('users').insert({ username: 'testuser' })
|
|
120
120
|
const user = await db('users').where('username', 'testuser').first()
|
|
121
|
-
await db('posts').insert({ title: 'Test Post',
|
|
121
|
+
await db('posts').insert({ title: 'Test Post', user_id: user.id })
|
|
122
122
|
|
|
123
123
|
const posts = await db('posts')
|
|
124
|
-
.join('users', 'posts.
|
|
124
|
+
.join('users', 'posts.user_id', 'users.id')
|
|
125
125
|
.select('posts.title', 'users.username')
|
|
126
126
|
|
|
127
127
|
assert.equal(posts.length, 1)
|
|
@@ -173,10 +173,11 @@ describe('dbTablesOperations.createKnexTable', () => {
|
|
|
173
173
|
})
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
-
it('
|
|
176
|
+
it('defaults storage columns to snake_case and keeps per-field overrides', async () => {
|
|
177
177
|
const schema = makeTableSchema({
|
|
178
|
-
displayName: { type: 'string', required: true
|
|
179
|
-
lastSeenAt: { type: 'dateTime'
|
|
178
|
+
displayName: { type: 'string', required: true },
|
|
179
|
+
lastSeenAt: { type: 'dateTime' },
|
|
180
|
+
externalRef: { type: 'string', storage: { column: 'legacy_ref' } }
|
|
180
181
|
})
|
|
181
182
|
|
|
182
183
|
await createKnexTable(db, makeSchemaInfo('mapped_profiles'), schema)
|
|
@@ -185,7 +186,9 @@ describe('dbTablesOperations.createKnexTable', () => {
|
|
|
185
186
|
assert.ok(info.id)
|
|
186
187
|
assert.ok(info.display_name)
|
|
187
188
|
assert.ok(info.last_seen_at)
|
|
189
|
+
assert.ok(info.legacy_ref)
|
|
188
190
|
assert.equal(info.displayName, undefined)
|
|
191
|
+
assert.equal(info.external_ref, undefined)
|
|
189
192
|
})
|
|
190
193
|
|
|
191
194
|
it('supports logical id fields mapped to custom storage columns', async () => {
|
|
@@ -221,10 +224,10 @@ describe('dbTablesOperations.createKnexTable', () => {
|
|
|
221
224
|
`)
|
|
222
225
|
|
|
223
226
|
const schema = makeTableSchema({
|
|
224
|
-
workspaceId: { type: 'id', required: true
|
|
225
|
-
userId: { type: 'id', required: true
|
|
227
|
+
workspaceId: { type: 'id', required: true },
|
|
228
|
+
userId: { type: 'id', required: true },
|
|
226
229
|
role: { type: 'string', enum: ['owner', 'member'], required: true, defaultTo: 'member' },
|
|
227
|
-
noteCount: { type: 'number', defaultTo: 0
|
|
230
|
+
noteCount: { type: 'number', defaultTo: 0 }
|
|
228
231
|
}, {
|
|
229
232
|
indexes: [
|
|
230
233
|
{
|
|
@@ -352,20 +355,22 @@ describe('dbTablesOperations.generateKnexMigration', () => {
|
|
|
352
355
|
assert.ok(migration.includes("table.string('title', 200).notNullable()"))
|
|
353
356
|
assert.ok(migration.includes("table.decimal('price', 10, 2)"))
|
|
354
357
|
assert.ok(migration.includes(".defaultTo(true)"))
|
|
355
|
-
assert.ok(migration.includes("table.foreign(['
|
|
358
|
+
assert.ok(migration.includes("table.foreign(['category_id'], 'products_category_id_foreign').references(['id']).inTable('categories').onDelete('SET NULL')"))
|
|
356
359
|
assert.ok(migration.includes('table.timestamps(true, true)'))
|
|
357
360
|
})
|
|
358
361
|
|
|
359
|
-
it('uses
|
|
362
|
+
it('uses default snake_case columns in generated migrations and keeps per-field overrides', () => {
|
|
360
363
|
const schema = makeTableSchema({
|
|
361
|
-
displayName: { type: 'string', required: true
|
|
362
|
-
loginCount: { type: 'number', defaultTo: 0
|
|
364
|
+
displayName: { type: 'string', required: true },
|
|
365
|
+
loginCount: { type: 'number', defaultTo: 0 },
|
|
366
|
+
externalRef: { type: 'string', storage: { column: 'legacy_ref' } }
|
|
363
367
|
})
|
|
364
368
|
|
|
365
369
|
const migration = generateKnexMigration('mapped_profiles', schema)
|
|
366
370
|
|
|
367
371
|
assert.ok(migration.includes("table.string('display_name').notNullable()"))
|
|
368
372
|
assert.ok(migration.includes("table.float('login_count').defaultTo(0)"))
|
|
373
|
+
assert.ok(migration.includes("table.string('legacy_ref')"))
|
|
369
374
|
assert.ok(!migration.includes("table.string('displayName')"))
|
|
370
375
|
})
|
|
371
376
|
|
|
@@ -384,11 +389,11 @@ describe('dbTablesOperations.generateKnexMigration', () => {
|
|
|
384
389
|
|
|
385
390
|
it('includes named composite indexes, foreign keys, checks, enum, and set columns in generated migrations', () => {
|
|
386
391
|
const schema = makeTableSchema({
|
|
387
|
-
workspaceId: { type: 'id', required: true
|
|
388
|
-
userId: { type: 'id', required: true
|
|
392
|
+
workspaceId: { type: 'id', required: true },
|
|
393
|
+
userId: { type: 'id', required: true },
|
|
389
394
|
role: { type: 'string', enum: ['owner', 'member'], required: true, defaultTo: 'member' },
|
|
390
395
|
flags: { type: 'string', setValues: ['featured', 'archived'] },
|
|
391
|
-
noteCount: { type: 'number', defaultTo: 0
|
|
396
|
+
noteCount: { type: 'number', defaultTo: 0 }
|
|
392
397
|
}, {
|
|
393
398
|
indexes: [
|
|
394
399
|
{
|
|
@@ -502,10 +507,10 @@ describe('dbTablesOperations.generateKnexMigrationDiff', () => {
|
|
|
502
507
|
idColumn: 'id'
|
|
503
508
|
})
|
|
504
509
|
const desiredSchema = makeTableSchema({
|
|
505
|
-
workspaceId: { type: 'id', required: true
|
|
506
|
-
userId: { type: 'id', required: true
|
|
510
|
+
workspaceId: { type: 'id', required: true },
|
|
511
|
+
userId: { type: 'id', required: true },
|
|
507
512
|
role: { type: 'string', enum: ['owner', 'member'], required: true, defaultTo: 'member' },
|
|
508
|
-
noteCount: { type: 'number', defaultTo: 0
|
|
513
|
+
noteCount: { type: 'number', defaultTo: 0 }
|
|
509
514
|
}, {
|
|
510
515
|
indexes: [
|
|
511
516
|
{
|
|
@@ -737,8 +742,8 @@ describe('RestApiKnexPlugin migration scope methods', () => {
|
|
|
737
742
|
await api.addResource('memberships', {
|
|
738
743
|
schema: {
|
|
739
744
|
id: { type: 'id' },
|
|
740
|
-
workspaceId: { type: 'id', required: true
|
|
741
|
-
userId: { type: 'id', required: true
|
|
745
|
+
workspaceId: { type: 'id', required: true },
|
|
746
|
+
userId: { type: 'id', required: true },
|
|
742
747
|
role: { type: 'string', enum: ['owner', 'member'], required: true, defaultTo: 'member' }
|
|
743
748
|
},
|
|
744
749
|
indexes: [
|
|
@@ -2,12 +2,27 @@ import { describe, it, beforeEach } from 'node:test'
|
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
import { Api } from 'hooked-api'
|
|
4
4
|
import { RestApiPlugin, FastifyPlugin } from '../index.js'
|
|
5
|
+
import { getRequestContracts } from '../plugins/core/lib/querying-writing/request-contracts.js'
|
|
5
6
|
import {
|
|
6
7
|
FakeFastifyApp,
|
|
7
8
|
FakeReply,
|
|
8
9
|
findFastifyRoute
|
|
9
10
|
} from './helpers/fake-fastify.js'
|
|
10
11
|
|
|
12
|
+
function resolveSchemaNode (rootSchema, schemaNode) {
|
|
13
|
+
if (!schemaNode || !Array.isArray(schemaNode.allOf) || schemaNode.allOf.length !== 1) {
|
|
14
|
+
return schemaNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ref = schemaNode.allOf[0]?.$ref || ''
|
|
18
|
+
if (!ref.startsWith('#/definitions/')) {
|
|
19
|
+
return schemaNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const definitionName = ref.slice('#/definitions/'.length)
|
|
23
|
+
return rootSchema?.definitions?.[definitionName] || schemaNode
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
async function createFastifyConnectorApi () {
|
|
12
27
|
const app = new FakeFastifyApp()
|
|
13
28
|
const api = new Api({
|
|
@@ -61,6 +76,12 @@ describe('Fastify Plugin', () => {
|
|
|
61
76
|
const postRelationshipRoute = findFastifyRoute(app, 'POST', '/api/articles/:id/relationships/:relationshipName')
|
|
62
77
|
const patchRelationshipRoute = findFastifyRoute(app, 'PATCH', '/api/articles/:id/relationships/:relationshipName')
|
|
63
78
|
const deleteRelationshipRoute = findFastifyRoute(app, 'DELETE', '/api/articles/:id/relationships/:relationshipName')
|
|
79
|
+
const relationshipContracts = getRequestContracts({
|
|
80
|
+
scopeName: 'articles',
|
|
81
|
+
schemaInfo: api.resources.articles.vars.schemaInfo,
|
|
82
|
+
includeDepthLimit: api.resources.articles.vars.includeDepthLimit,
|
|
83
|
+
sortableFields: api.resources.articles.vars.sortableFields
|
|
84
|
+
})
|
|
64
85
|
|
|
65
86
|
assert.ok(postRoute?.schema?.body, 'POST route should have a body schema')
|
|
66
87
|
assert.ok(putRoute?.schema?.body, 'PUT route should have a body schema')
|
|
@@ -70,7 +91,7 @@ describe('Fastify Plugin', () => {
|
|
|
70
91
|
assert.ok(patchRelationshipRoute?.schema?.body, 'PATCH relationship route should have a body schema')
|
|
71
92
|
assert.ok(deleteRelationshipRoute?.schema?.body, 'DELETE relationship route should have a body schema')
|
|
72
93
|
|
|
73
|
-
const postDataSchema = postRoute.schema.body.properties.data
|
|
94
|
+
const postDataSchema = resolveSchemaNode(postRoute.schema.body, postRoute.schema.body.properties.data)
|
|
74
95
|
const postAttributes = postDataSchema.properties.attributes
|
|
75
96
|
|
|
76
97
|
assert.deepEqual(postDataSchema.required, ['type'])
|
|
@@ -78,14 +99,15 @@ describe('Fastify Plugin', () => {
|
|
|
78
99
|
assert.equal(postAttributes.properties.author_id, undefined, 'belongsTo foreign key should not be accepted in attributes')
|
|
79
100
|
assert.equal(postAttributes.properties.internal_summary, undefined, 'computed fields should not be accepted in attributes')
|
|
80
101
|
assert.equal(postAttributes.properties.id, undefined, 'resource ids should not appear in attributes')
|
|
81
|
-
|
|
102
|
+
const postRelationships = resolveSchemaNode(postRoute.schema.body, postDataSchema.properties.relationships)
|
|
103
|
+
assert.ok(postRelationships.properties.author, 'declared belongsTo relationships should be represented in the transport schema')
|
|
82
104
|
|
|
83
|
-
const putDataSchema = putRoute.schema.body.properties.data
|
|
105
|
+
const putDataSchema = resolveSchemaNode(putRoute.schema.body, putRoute.schema.body.properties.data)
|
|
84
106
|
assert.ok(putDataSchema.required.includes('id'), 'PUT body schema should require data.id')
|
|
85
107
|
|
|
86
|
-
const
|
|
87
|
-
assert.ok(Array.isArray(
|
|
88
|
-
assert.deepEqual(
|
|
108
|
+
const patchDataSchemaNode = patchRoute.schema.body.properties.data
|
|
109
|
+
assert.ok(Array.isArray(patchDataSchemaNode.anyOf), 'PATCH body schema should require at least one writable section')
|
|
110
|
+
assert.deepEqual(patchDataSchemaNode.anyOf, [
|
|
89
111
|
{ required: ['attributes'] },
|
|
90
112
|
{ required: ['relationships'] }
|
|
91
113
|
])
|
|
@@ -93,6 +115,27 @@ describe('Fastify Plugin', () => {
|
|
|
93
115
|
assert.equal(postRelationshipRoute.schema.body.properties.data.type, 'array')
|
|
94
116
|
assert.ok(Array.isArray(patchRelationshipRoute.schema.body.properties.data.anyOf))
|
|
95
117
|
assert.equal(deleteRelationshipRoute.schema.body.properties.data.type, 'array')
|
|
118
|
+
assert.deepEqual(
|
|
119
|
+
postRelationshipRoute.schema.body,
|
|
120
|
+
relationshipContracts.postRelationship.schema.toJsonSchema({
|
|
121
|
+
mode: relationshipContracts.postRelationship.mode,
|
|
122
|
+
additionalProperties: false
|
|
123
|
+
})
|
|
124
|
+
)
|
|
125
|
+
assert.deepEqual(
|
|
126
|
+
patchRelationshipRoute.schema.body,
|
|
127
|
+
relationshipContracts.patchRelationship.schema.toJsonSchema({
|
|
128
|
+
mode: relationshipContracts.patchRelationship.mode,
|
|
129
|
+
additionalProperties: false
|
|
130
|
+
})
|
|
131
|
+
)
|
|
132
|
+
assert.deepEqual(
|
|
133
|
+
deleteRelationshipRoute.schema.body,
|
|
134
|
+
relationshipContracts.deleteRelationship.schema.toJsonSchema({
|
|
135
|
+
mode: relationshipContracts.deleteRelationship.mode,
|
|
136
|
+
additionalProperties: false
|
|
137
|
+
})
|
|
138
|
+
)
|
|
96
139
|
})
|
|
97
140
|
|
|
98
141
|
it('registers the JSON:API content-type parser and rejects unsupported write content types before the route handler', async () => {
|
|
@@ -176,7 +176,7 @@ describe('Field Setters', () => {
|
|
|
176
176
|
await cleanTables(knex, ['setter_users'])
|
|
177
177
|
})
|
|
178
178
|
|
|
179
|
-
it('should
|
|
179
|
+
it('should reject setter errors and roll back the write', async () => {
|
|
180
180
|
// Create a resource with a failing setter
|
|
181
181
|
await api.addResource('error_test', {
|
|
182
182
|
schema: {
|
|
@@ -196,16 +196,16 @@ describe('Field Setters', () => {
|
|
|
196
196
|
})
|
|
197
197
|
await api.resources.error_test.createKnexTable()
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
await assert.rejects(
|
|
200
|
+
api.resources.error_test.post({
|
|
201
|
+
good_field: 'HELLO',
|
|
202
|
+
bad_field: 'world'
|
|
203
|
+
}),
|
|
204
|
+
/Setter for field 'bad_field' failed: Setter failed!/
|
|
205
|
+
)
|
|
203
206
|
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
assert.equal(fetchedRecord.good_field, 'hello')
|
|
207
|
-
// Bad field should keep validated value (error logged but not thrown)
|
|
208
|
-
assert.equal(fetchedRecord.bad_field, 'world')
|
|
207
|
+
const records = await api.resources.error_test.query()
|
|
208
|
+
assert.equal(records?.length || 0, 0)
|
|
209
209
|
|
|
210
210
|
await cleanTables(knex, ['setter_errors'])
|
|
211
211
|
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import knexLib from 'knex'
|
|
4
|
+
import { cleanTables } from './helpers/test-utils.js'
|
|
5
|
+
import { createFileUploadApi } from './fixtures/api-configs.js'
|
|
6
|
+
|
|
7
|
+
const knex = knexLib({
|
|
8
|
+
client: 'better-sqlite3',
|
|
9
|
+
connection: {
|
|
10
|
+
filename: ':memory:'
|
|
11
|
+
},
|
|
12
|
+
useNullAsDefault: true
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
let api
|
|
16
|
+
let detectorState
|
|
17
|
+
let storage
|
|
18
|
+
|
|
19
|
+
function createTrackingStorage () {
|
|
20
|
+
return {
|
|
21
|
+
uploaded: [],
|
|
22
|
+
deleted: [],
|
|
23
|
+
|
|
24
|
+
async upload (file) {
|
|
25
|
+
const url = `/uploads/${file.filename}`
|
|
26
|
+
this.uploaded.push(url)
|
|
27
|
+
return url
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async delete (url) {
|
|
31
|
+
this.deleted.push(url)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createTestFile ({ filename = 'upload.png', mimetype = 'image/png', cleanup } = {}) {
|
|
37
|
+
return {
|
|
38
|
+
filename,
|
|
39
|
+
mimetype,
|
|
40
|
+
size: 1,
|
|
41
|
+
data: Buffer.from('x'),
|
|
42
|
+
cleanup
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('File handling cleanup', () => {
|
|
47
|
+
before(async () => {
|
|
48
|
+
detectorState = { payload: null }
|
|
49
|
+
storage = createTrackingStorage()
|
|
50
|
+
api = await createFileUploadApi(knex, { detectorState, storage })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
after(async () => {
|
|
54
|
+
await knex.destroy()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
beforeEach(async () => {
|
|
58
|
+
await cleanTables(knex, ['file_documents'])
|
|
59
|
+
detectorState.payload = null
|
|
60
|
+
storage.uploaded = []
|
|
61
|
+
storage.deleted = []
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('cleans detector temp files when MIME validation rejects an upload', async () => {
|
|
65
|
+
let cleanupCalls = 0
|
|
66
|
+
detectorState.payload = {
|
|
67
|
+
fields: { title: 'Invalid MIME' },
|
|
68
|
+
files: {
|
|
69
|
+
attachment: createTestFile({
|
|
70
|
+
filename: 'invalid.txt',
|
|
71
|
+
mimetype: 'text/plain',
|
|
72
|
+
cleanup: async () => {
|
|
73
|
+
cleanupCalls++
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await assert.rejects(
|
|
80
|
+
api.resources.documents.post({
|
|
81
|
+
inputRecord: { data: { type: 'documents', attributes: {} } },
|
|
82
|
+
simplified: false
|
|
83
|
+
}),
|
|
84
|
+
/Invalid file type for field 'attachment'/
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
assert.equal(cleanupCalls, 1)
|
|
88
|
+
assert.deepEqual(storage.uploaded, [])
|
|
89
|
+
assert.deepEqual(storage.deleted, [])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('deletes uploaded files when a later write validation error rolls back', async () => {
|
|
93
|
+
let cleanupCalls = 0
|
|
94
|
+
detectorState.payload = {
|
|
95
|
+
fields: {},
|
|
96
|
+
files: {
|
|
97
|
+
attachment: createTestFile({
|
|
98
|
+
filename: 'valid.png',
|
|
99
|
+
cleanup: async () => {
|
|
100
|
+
cleanupCalls++
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await assert.rejects(
|
|
107
|
+
api.resources.documents.post({
|
|
108
|
+
inputRecord: { data: { type: 'documents', attributes: {} } },
|
|
109
|
+
simplified: false
|
|
110
|
+
}),
|
|
111
|
+
/Schema validation failed for resource attributes/
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
assert.equal(cleanupCalls, 1)
|
|
115
|
+
assert.deepEqual(storage.uploaded, ['/uploads/valid.png'])
|
|
116
|
+
assert.deepEqual(storage.deleted, ['/uploads/valid.png'])
|
|
117
|
+
})
|
|
118
|
+
})
|