json-rest-api 1.0.24 → 1.0.26
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 +2 -1
- package/docs/API.md +12 -9
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +5 -2
- 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_Row_Policies.md +605 -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/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +5 -4
- package/plugins/core/connectors/express-plugin.js +6 -1
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- 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/query/anyapi-query-adapter.js +36 -26
- package/plugins/core/lib/anyapi/schema-utils.js +1 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
- package/plugins/core/lib/querying/knex-window-queries.js +10 -77
- package/plugins/core/lib/querying/storage-adapter-utils.js +8 -8
- package/plugins/core/lib/querying/url-helpers.js +9 -21
- 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 +6 -1
- 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/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
- package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
- package/plugins/core/lib/storage/storage-adapter.js +74 -13
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
- package/plugins/core/rest-api-knex-plugin.js +70 -60
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
- package/plugins/core/rest-api-plugin-methods/common.js +85 -63
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
- package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
- package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
- package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
- package/plugins/core/rest-api-plugin-methods/post.js +4 -7
- package/plugins/core/rest-api-plugin-methods/put.js +4 -7
- package/plugins/core/rest-api-plugin.js +6 -36
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/storage/local-storage.js +44 -19
- package/plugins/storage/s3-storage.js +21 -55
- package/tests/README.md +1 -1
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/fastify-plugin.test.js +28 -0
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +383 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/local-storage.test.js +96 -0
- package/tests/pagination-cursor-multifield.test.js +121 -0
- 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 +489 -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 +1 -0
- package/tests/test-development-guide.md +5 -5
|
@@ -9,11 +9,17 @@ import {
|
|
|
9
9
|
resourceIdentifier,
|
|
10
10
|
} from './helpers/test-utils.js'
|
|
11
11
|
import { storageMode } from './helpers/storage-mode.js'
|
|
12
|
+
import { parseJsonApiQuery } from '../plugins/core/lib/querying-writing/connectors-query-parser.js'
|
|
12
13
|
|
|
13
14
|
const isAnyApi = storageMode.isAnyApi()
|
|
14
15
|
|
|
15
16
|
const maybeDescribe = isAnyApi ? describe : describe.skip
|
|
16
17
|
|
|
18
|
+
const parseLinkQuery = (link) => {
|
|
19
|
+
const url = new URL(link, 'https://example.test')
|
|
20
|
+
return parseJsonApiQuery(url.search.slice(1))
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
maybeDescribe('AnyAPI Cursor Pagination', () => {
|
|
18
24
|
const knex = knexLib({
|
|
19
25
|
client: 'better-sqlite3',
|
|
@@ -78,7 +84,7 @@ maybeDescribe('AnyAPI Cursor Pagination', () => {
|
|
|
78
84
|
|
|
79
85
|
assert.equal(result.data.length, 2)
|
|
80
86
|
assert(result.meta?.pagination?.cursor?.next, 'Should include next cursor')
|
|
81
|
-
assert(result.links?.next
|
|
87
|
+
assert(parseLinkQuery(result.links?.next).page.after, 'Next link should include cursor parameter')
|
|
82
88
|
})
|
|
83
89
|
|
|
84
90
|
it('paginates forward using page[after]', async () => {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, before, beforeEach, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import knexLib from 'knex'
|
|
4
|
+
import { Api } from 'hooked-api'
|
|
5
|
+
|
|
6
|
+
import { RestApiPlugin, RestApiAnyapiKnexPlugin } from '../index.js'
|
|
7
|
+
import { ensureAnyApiSchema } from '../plugins/core/lib/anyapi/schema-utils.js'
|
|
8
|
+
import { storageMode } from './helpers/storage-mode.js'
|
|
9
|
+
import {
|
|
10
|
+
createJsonApiDocument,
|
|
11
|
+
createRelationship,
|
|
12
|
+
resourceIdentifier,
|
|
13
|
+
} from './helpers/test-utils.js'
|
|
14
|
+
|
|
15
|
+
const maybeDescribe = storageMode.isAnyApi() ? describe : describe.skip
|
|
16
|
+
|
|
17
|
+
maybeDescribe('AnyAPI custom idProperty', () => {
|
|
18
|
+
const knex = knexLib({
|
|
19
|
+
client: 'better-sqlite3',
|
|
20
|
+
connection: { filename: ':memory:' },
|
|
21
|
+
useNullAsDefault: true,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
let api
|
|
25
|
+
|
|
26
|
+
before(async () => {
|
|
27
|
+
await ensureAnyApiSchema(knex)
|
|
28
|
+
|
|
29
|
+
api = new Api({ name: 'anyapi-custom-idproperty-test', log: { level: 'warn' } })
|
|
30
|
+
await api.use(RestApiPlugin, {
|
|
31
|
+
simplifiedApi: false,
|
|
32
|
+
simplifiedTransport: false,
|
|
33
|
+
returnRecordApi: {
|
|
34
|
+
post: 'full',
|
|
35
|
+
put: 'full',
|
|
36
|
+
patch: 'full',
|
|
37
|
+
},
|
|
38
|
+
sortableFields: ['id', 'owner_id', 'item_id', 'code', 'name', 'category'],
|
|
39
|
+
queryDefaultLimit: 20,
|
|
40
|
+
queryMaxLimit: 100,
|
|
41
|
+
})
|
|
42
|
+
await api.use(RestApiAnyapiKnexPlugin, { knex })
|
|
43
|
+
|
|
44
|
+
await api.addResource('owners', {
|
|
45
|
+
schema: {
|
|
46
|
+
owner_id: { type: 'id' },
|
|
47
|
+
name: { type: 'string', required: true },
|
|
48
|
+
},
|
|
49
|
+
idProperty: 'owner_id',
|
|
50
|
+
tableName: 'custom_id_owners',
|
|
51
|
+
})
|
|
52
|
+
await api.resources.owners.createKnexTable()
|
|
53
|
+
|
|
54
|
+
await api.addResource('items', {
|
|
55
|
+
schema: {
|
|
56
|
+
item_id: { type: 'id' },
|
|
57
|
+
code: { type: 'string', required: true, search: true },
|
|
58
|
+
name: { type: 'string', required: true },
|
|
59
|
+
category: { type: 'string', required: true },
|
|
60
|
+
owner_id: { type: 'id', belongsTo: 'owners', as: 'owner' },
|
|
61
|
+
},
|
|
62
|
+
idProperty: 'item_id',
|
|
63
|
+
tableName: 'custom_id_items',
|
|
64
|
+
})
|
|
65
|
+
await api.resources.items.createKnexTable()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
after(async () => {
|
|
69
|
+
await api?.release()
|
|
70
|
+
await knex.destroy()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
beforeEach(async () => {
|
|
74
|
+
await knex('any_links').delete()
|
|
75
|
+
await knex('any_records').delete()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('uses logical ids for post, get, query, sparse fields, cursors, and includes', async () => {
|
|
79
|
+
const owner = await api.resources.owners.post({
|
|
80
|
+
inputRecord: createJsonApiDocument('owners', { name: 'Owner One' }),
|
|
81
|
+
simplified: false,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const itemInputs = [
|
|
85
|
+
{ code: 'A001', name: 'Apple', category: 'Fruit' },
|
|
86
|
+
{ code: 'A002', name: 'Apricot', category: 'Fruit' },
|
|
87
|
+
{ code: 'B001', name: 'Broccoli', category: 'Vegetable' },
|
|
88
|
+
{ code: 'C001', name: 'Carrot', category: 'Vegetable' },
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
const createdItems = []
|
|
92
|
+
for (const item of itemInputs) {
|
|
93
|
+
const created = await api.resources.items.post({
|
|
94
|
+
inputRecord: createJsonApiDocument(
|
|
95
|
+
'items',
|
|
96
|
+
item,
|
|
97
|
+
{ owner: createRelationship(resourceIdentifier('owners', owner.data.id)) }
|
|
98
|
+
),
|
|
99
|
+
simplified: false,
|
|
100
|
+
})
|
|
101
|
+
createdItems.push(created)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fetched = await api.resources.items.get({
|
|
105
|
+
id: createdItems[0].data.id,
|
|
106
|
+
queryParams: {
|
|
107
|
+
include: ['owner'],
|
|
108
|
+
fields: {
|
|
109
|
+
items: 'item_id,code,name,category',
|
|
110
|
+
owners: 'owner_id,name',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
simplified: false,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
assert.equal(fetched.data.id, createdItems[0].data.id)
|
|
117
|
+
assert.equal(fetched.data.attributes.code, 'A001')
|
|
118
|
+
assert.equal(fetched.data.attributes.name, 'Apple')
|
|
119
|
+
assert.equal(fetched.data.attributes.item_id, undefined)
|
|
120
|
+
assert.equal(fetched.included?.[0]?.type, 'owners')
|
|
121
|
+
assert.equal(fetched.included?.[0]?.id, owner.data.id)
|
|
122
|
+
assert.equal(fetched.included?.[0]?.attributes.owner_id, undefined)
|
|
123
|
+
|
|
124
|
+
const firstPage = await api.resources.items.query({
|
|
125
|
+
queryParams: {
|
|
126
|
+
include: ['owner'],
|
|
127
|
+
fields: {
|
|
128
|
+
items: 'item_id,code,name,category',
|
|
129
|
+
owners: 'owner_id,name',
|
|
130
|
+
},
|
|
131
|
+
sort: ['category', 'name'],
|
|
132
|
+
page: { size: 2 },
|
|
133
|
+
},
|
|
134
|
+
simplified: false,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
assert.equal(firstPage.data.length, 2)
|
|
138
|
+
assert(firstPage.meta.pagination.cursor.next, 'first page should include a next cursor')
|
|
139
|
+
assert(firstPage.data.every((item) => item.attributes.item_id === undefined))
|
|
140
|
+
assert(firstPage.included?.some((entry) => entry.type === 'owners' && entry.id === owner.data.id))
|
|
141
|
+
|
|
142
|
+
const secondPage = await api.resources.items.query({
|
|
143
|
+
queryParams: {
|
|
144
|
+
include: ['owner'],
|
|
145
|
+
fields: {
|
|
146
|
+
items: 'item_id,code,name,category',
|
|
147
|
+
owners: 'owner_id,name',
|
|
148
|
+
},
|
|
149
|
+
sort: ['category', 'name'],
|
|
150
|
+
page: {
|
|
151
|
+
size: 2,
|
|
152
|
+
after: firstPage.meta.pagination.cursor.next,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
simplified: false,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const firstIds = firstPage.data.map((item) => item.id)
|
|
159
|
+
const secondIds = secondPage.data.map((item) => item.id)
|
|
160
|
+
assert.equal(secondPage.data.length, 2)
|
|
161
|
+
assert(!secondIds.some((id) => firstIds.includes(id)), 'cursor page should not repeat records')
|
|
162
|
+
})
|
|
163
|
+
})
|
|
@@ -2,6 +2,7 @@ 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,
|
|
@@ -75,6 +76,12 @@ describe('Fastify Plugin', () => {
|
|
|
75
76
|
const postRelationshipRoute = findFastifyRoute(app, 'POST', '/api/articles/:id/relationships/:relationshipName')
|
|
76
77
|
const patchRelationshipRoute = findFastifyRoute(app, 'PATCH', '/api/articles/:id/relationships/:relationshipName')
|
|
77
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
|
+
})
|
|
78
85
|
|
|
79
86
|
assert.ok(postRoute?.schema?.body, 'POST route should have a body schema')
|
|
80
87
|
assert.ok(putRoute?.schema?.body, 'PUT route should have a body schema')
|
|
@@ -108,6 +115,27 @@ describe('Fastify Plugin', () => {
|
|
|
108
115
|
assert.equal(postRelationshipRoute.schema.body.properties.data.type, 'array')
|
|
109
116
|
assert.ok(Array.isArray(patchRelationshipRoute.schema.body.properties.data.anyOf))
|
|
110
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
|
+
)
|
|
111
139
|
})
|
|
112
140
|
|
|
113
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
|
+
})
|