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
package/scripts/run-docs.js
CHANGED
|
@@ -1,30 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Run the documentation site
|
|
5
|
-
*
|
|
6
|
-
* This script:
|
|
7
|
-
* 1. Syncs documentation files
|
|
8
|
-
* 2. Builds the Jekyll site
|
|
9
|
-
* 3. Starts the server
|
|
10
|
-
* 4. Opens the browser
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
3
|
import { spawn } from 'child_process'
|
|
14
|
-
import { promises as fs } from 'fs'
|
|
15
4
|
import path from 'path'
|
|
16
5
|
import { fileURLToPath } from 'url'
|
|
17
6
|
|
|
18
7
|
const __filename = fileURLToPath(import.meta.url)
|
|
19
8
|
const __dirname = path.dirname(__filename)
|
|
20
9
|
const projectRoot = path.join(__dirname, '..')
|
|
10
|
+
const docsRoot = path.join(projectRoot, 'docs')
|
|
11
|
+
const LOCAL_URL = 'http://localhost:4000/json-rest-api/'
|
|
12
|
+
const BROWSER_DELAY_MS = 3000
|
|
21
13
|
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
function parseMode (argv = []) {
|
|
15
|
+
if (argv.includes('--serve') || argv.includes('--dev')) {
|
|
16
|
+
return 'serve'
|
|
17
|
+
}
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const GITHUB_PAGES_URL = 'https://mobily-enterprises.github.io/json-rest-api/'
|
|
19
|
+
return 'build'
|
|
20
|
+
}
|
|
28
21
|
|
|
29
22
|
async function runCommand (command, args, options = {}) {
|
|
30
23
|
return new Promise((resolve, reject) => {
|
|
@@ -38,100 +31,103 @@ async function runCommand (command, args, options = {}) {
|
|
|
38
31
|
proc.on('exit', (code) => {
|
|
39
32
|
if (code === 0) {
|
|
40
33
|
resolve()
|
|
41
|
-
|
|
42
|
-
reject(new Error(`Command failed with code ${code}`))
|
|
34
|
+
return
|
|
43
35
|
}
|
|
36
|
+
|
|
37
|
+
reject(new Error(`Command failed with code ${code}`))
|
|
44
38
|
})
|
|
45
39
|
})
|
|
46
40
|
}
|
|
47
41
|
|
|
42
|
+
async function ensureBundlerDependencies () {
|
|
43
|
+
try {
|
|
44
|
+
await runCommand('bundle', ['check'], { cwd: docsRoot })
|
|
45
|
+
} catch {
|
|
46
|
+
console.log('Installing Jekyll dependencies...')
|
|
47
|
+
await runCommand('bundle', ['install'], { cwd: docsRoot })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
48
51
|
async function openBrowser (url) {
|
|
49
52
|
const platform = process.platform
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
command = 'start'
|
|
56
|
-
} else {
|
|
57
|
-
// Linux
|
|
58
|
-
command = 'xdg-open'
|
|
59
|
-
}
|
|
53
|
+
const command = platform === 'darwin'
|
|
54
|
+
? 'open'
|
|
55
|
+
: platform === 'win32'
|
|
56
|
+
? 'start'
|
|
57
|
+
: 'xdg-open'
|
|
60
58
|
|
|
61
59
|
try {
|
|
62
60
|
await runCommand(command, [url])
|
|
63
|
-
console.log(
|
|
64
|
-
} catch
|
|
65
|
-
console.log(
|
|
61
|
+
console.log(`Opened browser at ${url}`)
|
|
62
|
+
} catch {
|
|
63
|
+
console.log(`Visit ${url} in your browser`)
|
|
66
64
|
}
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
async function
|
|
70
|
-
console.log('
|
|
67
|
+
async function buildDocs () {
|
|
68
|
+
console.log('Building documentation site...')
|
|
69
|
+
await runCommand('bundle', ['exec', 'jekyll', 'build'], { cwd: docsRoot })
|
|
70
|
+
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
async function serveDocs () {
|
|
73
|
+
console.log('Starting documentation server...')
|
|
74
|
+
|
|
75
|
+
const jekyll = spawn('bundle', ['exec', 'jekyll', 'serve', '--watch'], {
|
|
76
|
+
cwd: docsRoot,
|
|
77
|
+
stdio: 'pipe',
|
|
78
|
+
shell: true
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
jekyll.stdout.on('data', (data) => {
|
|
82
|
+
const output = data.toString()
|
|
83
|
+
process.stdout.write(output)
|
|
84
|
+
|
|
85
|
+
if (output.includes('Server running') || output.includes('Server address:')) {
|
|
86
|
+
setTimeout(() => openBrowser(LOCAL_URL), 1000)
|
|
84
87
|
}
|
|
88
|
+
})
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
cwd: path.join(projectRoot, 'docs'),
|
|
90
|
-
stdio: 'pipe',
|
|
91
|
-
shell: true
|
|
92
|
-
})
|
|
90
|
+
jekyll.stderr.on('data', (data) => {
|
|
91
|
+
process.stderr.write(data)
|
|
92
|
+
})
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
process.on('SIGINT', () => {
|
|
95
|
+
console.log('\nShutting down Jekyll server...')
|
|
96
|
+
jekyll.kill()
|
|
97
|
+
process.exit(0)
|
|
98
|
+
})
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
})
|
|
100
|
+
jekyll.on('error', (err) => {
|
|
101
|
+
console.error('Jekyll server error:', err)
|
|
102
|
+
process.exit(1)
|
|
103
|
+
})
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
jekyll.on('exit', (code) => {
|
|
106
|
+
if (code !== 0 && code !== null) {
|
|
107
|
+
console.error(`Jekyll server exited with code ${code}`)
|
|
108
|
+
process.exit(code)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
108
111
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
console.log('\n\n👋 Shutting down Jekyll server...')
|
|
112
|
-
jekyll.kill()
|
|
113
|
-
process.exit(0)
|
|
114
|
-
})
|
|
112
|
+
setTimeout(() => openBrowser(LOCAL_URL), BROWSER_DELAY_MS)
|
|
113
|
+
}
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
process.exit(1)
|
|
119
|
-
})
|
|
115
|
+
async function main () {
|
|
116
|
+
const mode = parseMode(process.argv.slice(2))
|
|
120
117
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
118
|
+
try {
|
|
119
|
+
await ensureBundlerDependencies()
|
|
120
|
+
|
|
121
|
+
if (mode === 'serve') {
|
|
122
|
+
await serveDocs()
|
|
123
|
+
return
|
|
124
|
+
}
|
|
127
125
|
|
|
128
|
-
|
|
129
|
-
setTimeout(() => openBrowser(LOCAL_URL), BROWSER_DELAY)
|
|
126
|
+
await buildDocs()
|
|
130
127
|
} catch (error) {
|
|
131
|
-
console.error('
|
|
128
|
+
console.error('Error:', error.message)
|
|
132
129
|
process.exit(1)
|
|
133
130
|
}
|
|
134
131
|
}
|
|
135
132
|
|
|
136
|
-
// Run the script
|
|
137
133
|
main()
|
package/test-debug.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createBasicApi } from './tests/fixtures/api-configs.js'
|
|
2
|
-
import { cleanTables, createJsonApiDocument
|
|
2
|
+
import { cleanTables, createJsonApiDocument } from './tests/helpers/test-utils.js'
|
|
3
3
|
import knex from 'knex'
|
|
4
4
|
|
|
5
5
|
const dbConfig = {
|
package/tests/README.md
CHANGED
|
@@ -20,7 +20,7 @@ All tests MUST use `simplified: false` to ensure JSON:API compliance:
|
|
|
20
20
|
const result = await api.resources.resourceName.method({
|
|
21
21
|
inputRecord: jsonApiDocument,
|
|
22
22
|
simplified: false,
|
|
23
|
-
returnFullRecord:
|
|
23
|
+
returnFullRecord: 'full' // Use 'no', 'minimal', or 'full' based on needs
|
|
24
24
|
});
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -163,9 +163,16 @@ const result = await api.resources.books.query({
|
|
|
163
163
|
# Run all tests
|
|
164
164
|
npm test
|
|
165
165
|
|
|
166
|
+
# Run the focused normalizeId verification path plus docs rebuild
|
|
167
|
+
npm run verify
|
|
168
|
+
|
|
166
169
|
# Run all tests against the AnyAPI storage backend
|
|
167
170
|
npm run test:anyapi
|
|
168
171
|
|
|
172
|
+
# Run only the normalizeId contract suites
|
|
173
|
+
npm run test:id-contracts
|
|
174
|
+
npm run test:id-contracts:anyapi
|
|
175
|
+
|
|
169
176
|
# Windows PowerShell (AnyAPI run)
|
|
170
177
|
$Env:JSON_REST_API_STORAGE = 'anyapi'; npm test
|
|
171
178
|
|
|
@@ -70,6 +70,7 @@ describe('AnyAPI Knex Plugin - Basic Attributes', () => {
|
|
|
70
70
|
assert.equal(result.data.attributes.name, 'Australia')
|
|
71
71
|
|
|
72
72
|
const stored = await knex('any_records').first()
|
|
73
|
+
assert.equal(stored.logical_id, result.data.id)
|
|
73
74
|
assert.equal(stored.string_1, 'Australia')
|
|
74
75
|
assert.equal(stored.string_2, 'AU')
|
|
75
76
|
|
|
@@ -83,11 +84,13 @@ describe('AnyAPI Knex Plugin - Basic Attributes', () => {
|
|
|
83
84
|
|
|
84
85
|
it('lists records', async () => {
|
|
85
86
|
await knex('any_records').insert([{
|
|
87
|
+
logical_id: '1',
|
|
86
88
|
tenant_id: 'default',
|
|
87
89
|
resource: 'countries',
|
|
88
90
|
string_1: 'Canada',
|
|
89
91
|
string_2: 'CA',
|
|
90
92
|
}, {
|
|
93
|
+
logical_id: '2',
|
|
91
94
|
tenant_id: 'default',
|
|
92
95
|
resource: 'countries',
|
|
93
96
|
string_1: 'Brazil',
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import knexLib from 'knex'
|
|
4
|
+
import { Api } from 'hooked-api'
|
|
5
|
+
import { RestApiPlugin } from '../plugins/core/rest-api-plugin.js'
|
|
6
|
+
import { RestApiKnexPlugin } from '../plugins/core/rest-api-knex-plugin.js'
|
|
7
|
+
import {
|
|
8
|
+
cleanTables,
|
|
9
|
+
validateJsonApiStructure
|
|
10
|
+
} from './helpers/test-utils.js'
|
|
11
|
+
|
|
12
|
+
const knex = knexLib({
|
|
13
|
+
client: 'better-sqlite3',
|
|
14
|
+
connection: {
|
|
15
|
+
filename: ':memory:'
|
|
16
|
+
},
|
|
17
|
+
useNullAsDefault: true
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
let api
|
|
21
|
+
|
|
22
|
+
function createMysqlStyleInsertDb (baseKnex) {
|
|
23
|
+
return (tableName) => {
|
|
24
|
+
const query = baseKnex(tableName)
|
|
25
|
+
const originalInsert = query.insert.bind(query)
|
|
26
|
+
|
|
27
|
+
query.insert = (...args) => {
|
|
28
|
+
const insertBuilder = originalInsert(...args)
|
|
29
|
+
const originalReturning = insertBuilder.returning.bind(insertBuilder)
|
|
30
|
+
|
|
31
|
+
insertBuilder.returning = async (...returnArgs) => {
|
|
32
|
+
await originalReturning(...returnArgs)
|
|
33
|
+
return [0]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return insertBuilder
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return query
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('Custom id POST fallback when insert returning is unusable', () => {
|
|
44
|
+
before(async () => {
|
|
45
|
+
api = new Api({
|
|
46
|
+
name: 'custom-id-mysql-returning-fallback-test'
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
await api.use(RestApiPlugin, {
|
|
50
|
+
simplifiedApi: true,
|
|
51
|
+
simplifiedTransport: false,
|
|
52
|
+
returnRecordApi: {
|
|
53
|
+
post: 'full',
|
|
54
|
+
put: 'full',
|
|
55
|
+
patch: 'full'
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
await api.use(RestApiKnexPlugin, { knex })
|
|
59
|
+
|
|
60
|
+
await api.addResource('user_settings', {
|
|
61
|
+
tableName: 'mysqlish_user_settings',
|
|
62
|
+
idProperty: 'user_id',
|
|
63
|
+
searchSchema: {
|
|
64
|
+
id: { type: 'id', actualField: 'id' }
|
|
65
|
+
},
|
|
66
|
+
schema: {
|
|
67
|
+
theme: { type: 'string', required: true, max: 32 }
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
await api.resources.user_settings.createKnexTable()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
after(async () => {
|
|
74
|
+
await knex.destroy()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
beforeEach(async () => {
|
|
78
|
+
await cleanTables(knex, ['mysqlish_user_settings'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('returns the explicit id for simplified POST when the insert result is 0', async () => {
|
|
82
|
+
const mysqlStyleDb = createMysqlStyleInsertDb(knex)
|
|
83
|
+
|
|
84
|
+
const created = await api.resources.user_settings.post({
|
|
85
|
+
inputRecord: {
|
|
86
|
+
id: '6',
|
|
87
|
+
theme: 'dark'
|
|
88
|
+
},
|
|
89
|
+
simplified: true,
|
|
90
|
+
transaction: mysqlStyleDb
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
assert.equal(created.id, '6')
|
|
94
|
+
assert.equal(created.theme, 'dark')
|
|
95
|
+
|
|
96
|
+
const dbRow = await knex('mysqlish_user_settings').where('user_id', 6).first()
|
|
97
|
+
assert.ok(dbRow)
|
|
98
|
+
assert.equal(String(dbRow.user_id), '6')
|
|
99
|
+
assert.equal(dbRow.theme, 'dark')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('returns the explicit id for non-simplified POST when the insert result is 0', async () => {
|
|
103
|
+
const mysqlStyleDb = createMysqlStyleInsertDb(knex)
|
|
104
|
+
|
|
105
|
+
const created = await api.resources.user_settings.post({
|
|
106
|
+
inputRecord: {
|
|
107
|
+
data: {
|
|
108
|
+
type: 'user_settings',
|
|
109
|
+
id: '8',
|
|
110
|
+
attributes: {
|
|
111
|
+
theme: 'light'
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
simplified: false,
|
|
116
|
+
transaction: mysqlStyleDb
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
validateJsonApiStructure(created)
|
|
120
|
+
assert.equal(created.data.id, '8')
|
|
121
|
+
assert.equal(created.data.attributes.theme, 'light')
|
|
122
|
+
|
|
123
|
+
const dbRow = await knex('mysqlish_user_settings').where('user_id', 8).first()
|
|
124
|
+
assert.ok(dbRow)
|
|
125
|
+
assert.equal(String(dbRow.user_id), '8')
|
|
126
|
+
assert.equal(dbRow.theme, 'light')
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -5,6 +5,7 @@ import knexLib from 'knex'
|
|
|
5
5
|
import { introspectKnexTableSnapshot } from '../plugins/core/lib/dbIntrospection.js'
|
|
6
6
|
import { createKnexTable } from '../plugins/core/lib/dbTablesOperations.js'
|
|
7
7
|
import { createBasicApi } from './fixtures/api-configs.js'
|
|
8
|
+
import { storageMode } from './helpers/storage-mode.js'
|
|
8
9
|
|
|
9
10
|
function createMysqlKnexRawDouble ({
|
|
10
11
|
schemaName = 'appdb',
|
|
@@ -484,7 +485,9 @@ describe('dbIntrospection.introspectKnexTableSnapshot (mysql raw double)', () =>
|
|
|
484
485
|
})
|
|
485
486
|
})
|
|
486
487
|
|
|
487
|
-
|
|
488
|
+
const describeKnexIntrospection = storageMode.isAnyApi() ? describe.skip : describe
|
|
489
|
+
|
|
490
|
+
describeKnexIntrospection('RestApiKnexPlugin introspection scope method', () => {
|
|
488
491
|
let db
|
|
489
492
|
let api
|
|
490
493
|
|