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
@@ -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
- // Delay before opening browser (milliseconds)
23
- const BROWSER_DELAY = 3000
14
+ function parseMode (argv = []) {
15
+ if (argv.includes('--serve') || argv.includes('--dev')) {
16
+ return 'serve'
17
+ }
24
18
 
25
- // Site URLs
26
- const LOCAL_URL = 'http://localhost:4000/json-rest-api/'
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
- } else {
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
- let command
51
-
52
- if (platform === 'darwin') {
53
- command = 'open'
54
- } else if (platform === 'win32') {
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(`\n🌐 Opened browser at ${url}`)
64
- } catch (err) {
65
- console.log(`\n💡 Visit ${url} in your browser`)
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 main () {
70
- console.log('📚 Starting documentation site...\n')
67
+ async function buildDocs () {
68
+ console.log('Building documentation site...')
69
+ await runCommand('bundle', ['exec', 'jekyll', 'build'], { cwd: docsRoot })
70
+ }
71
71
 
72
- try {
73
- // Step 1: There is no step 1
74
-
75
- // Step 2: Install Jekyll dependencies if needed
76
- const bundleLockPath = path.join(projectRoot, 'docs', 'Gemfile.lock')
77
- try {
78
- await fs.access(bundleLockPath)
79
- } catch {
80
- console.log('\n2️⃣ Installing Jekyll dependencies...')
81
- await runCommand('bundle', ['install'], {
82
- cwd: path.join(projectRoot, 'docs')
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
- // Step 3: Start Jekyll server in background
87
- console.log('\n3️⃣ Starting Jekyll server...')
88
- const jekyll = spawn('bundle', ['exec', 'jekyll', 'serve', '--watch'], {
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
- // Handle server output
95
- jekyll.stdout.on('data', (data) => {
96
- const output = data.toString()
97
- process.stdout.write(output)
94
+ process.on('SIGINT', () => {
95
+ console.log('\nShutting down Jekyll server...')
96
+ jekyll.kill()
97
+ process.exit(0)
98
+ })
98
99
 
99
- // Open browser when server is ready
100
- if (output.includes('Server running') || output.includes('Server address:')) {
101
- setTimeout(() => openBrowser(LOCAL_URL), 1000)
102
- }
103
- })
100
+ jekyll.on('error', (err) => {
101
+ console.error('Jekyll server error:', err)
102
+ process.exit(1)
103
+ })
104
104
 
105
- jekyll.stderr.on('data', (data) => {
106
- process.stderr.write(data)
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
- // Handle shutdown
110
- process.on('SIGINT', () => {
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
- jekyll.on('error', (err) => {
117
- console.error('❌ Jekyll server error:', err)
118
- process.exit(1)
119
- })
115
+ async function main () {
116
+ const mode = parseMode(process.argv.slice(2))
120
117
 
121
- jekyll.on('exit', (code) => {
122
- if (code !== 0 && code !== null) {
123
- console.error(`❌ Jekyll server exited with code ${code}`)
124
- process.exit(code)
125
- }
126
- })
118
+ try {
119
+ await ensureBundlerDependencies()
120
+
121
+ if (mode === 'serve') {
122
+ await serveDocs()
123
+ return
124
+ }
127
125
 
128
- // Wait a bit then try to open browser anyway (in case we missed the server ready message)
129
- setTimeout(() => openBrowser(LOCAL_URL), BROWSER_DELAY)
126
+ await buildDocs()
130
127
  } catch (error) {
131
- console.error('❌ Error:', error.message)
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, createRelationship } from './tests/helpers/test-utils.js'
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: true/false // Based on needs
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?.includes('page[after]='), 'Next link should include cursor parameter')
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
- describe('RestApiKnexPlugin introspection scope method', () => {
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