json-rest-api 1.0.29 → 2.0.0

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 (292) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -0,0 +1,338 @@
1
+ ---
2
+ title: "Getting started"
3
+ chapter: 1
4
+ chapter_label: "01"
5
+ ---
6
+
7
+ # 01. Getting started
8
+
9
+ The guide uses a book catalog with countries, publishers, authors, books and
10
+ book-author links. Start with countries here; subsequent chapters add the
11
+ related resources. The [quickstart](../QUICKSTART.md) provides a complete
12
+ Express server with related publishers and authors.
13
+
14
+ Use Node 24 or newer; development verification runs on Node 24. These examples
15
+ use the revised contract in the [migration guide](33-migrating-to-v2.md). Until
16
+ its release is published, install the intended library tarball instead of the
17
+ registry package.
18
+
19
+ ## Install and create a resource
20
+
21
+ ```bash
22
+ npm init -y
23
+ npm pkg set type=module
24
+ npm install json-rest-api knex better-sqlite3
25
+ ```
26
+
27
+ Save this as `index.js`. The database is in memory, so records disappear when
28
+ the process exits. Resource schemas are flat field maps. Related resources are
29
+ not declared in this first example because their definitions come later.
30
+
31
+ ```javascript
32
+ import { JsonRestApi, RestApiPlugin, RestApiKnexPlugin } from 'json-rest-api'
33
+ import knexLib from 'knex'
34
+
35
+ const knex = knexLib({
36
+ client: 'better-sqlite3',
37
+ connection: { filename: ':memory:' },
38
+ useNullAsDefault: true
39
+ })
40
+ const api = new JsonRestApi({
41
+ name: 'book-catalog-api',
42
+ })
43
+
44
+ try {
45
+ await api.use(RestApiPlugin)
46
+ await api.use(RestApiKnexPlugin, { knex })
47
+ await api.addResource('countries', {
48
+ schema: {
49
+ name: { type: 'string', required: true, max: 100, search: true },
50
+ code: { type: 'string', max: 2, unique: true, search: true }
51
+ }
52
+ })
53
+ await api.resources.countries.createKnexTable()
54
+
55
+ const country = await api.resources.countries.post({
56
+ data: { name: 'United States', code: 'US' }
57
+ })
58
+ const refetched = await api.resources.countries.get({ id: country.id })
59
+ console.log('Country:', refetched)
60
+
61
+ const matches = await api.resources.countries.query({
62
+ queryParams: {
63
+ filters: { code: 'US' },
64
+ sort: ['name'],
65
+ page: { number: 1, size: 10 }
66
+ }
67
+ })
68
+ console.log('Matches:', matches.data)
69
+
70
+ const changed = await api.resources.countries.patch({
71
+ id: country.id,
72
+ data: { name: 'United States of America' },
73
+ returning: 'minimal'
74
+ })
75
+ console.log('Changed identifier:', changed)
76
+
77
+ const document = await api.resources.countries.get({
78
+ id: country.id,
79
+ format: 'jsonapi'
80
+ })
81
+ console.log('JSON:API attributes:', document.data.attributes)
82
+ } finally {
83
+ await knex.destroy()
84
+ }
85
+ ```
86
+
87
+ Run `node index.js`. The first record has name `United States` and code `US`;
88
+ the query returns that record. PATCH returns `{ type: 'countries', id: country.id }`.
89
+ The final JSON:API read has `data.attributes.name` equal to
90
+ `United States of America`.
91
+
92
+ When adapting later examples to this script, insert their operations inside the
93
+ `try` block after resource registration and before the database is destroyed.
94
+
95
+ ## Logging
96
+
97
+ Logging is silent unless you supply a logger. For example:
98
+
99
+ ```javascript
100
+ const api = new JsonRestApi({
101
+ name: 'app',
102
+ logger: { warn: console.warn, error: console.error }
103
+ })
104
+ ```
105
+
106
+ The logger can supply `trace`, `debug`, `info`, `warn`, `error` and `fatal` methods.
107
+ Missing methods are silent. Your logger controls formatting and filtering.
108
+ Resource and connector diagnostics use the library's bounded error formatting;
109
+ see the [API reference](../API.md) for diagnostic and error contracts.
110
+ The old `log` and `logging` constructor options are rejected.
111
+
112
+ ## Database configuration
113
+
114
+ `RestApiKnexPlugin` uses the supplied Knex instance. Install the driver for the
115
+ selected database. For PostgreSQL, use `npm install pg` and configure:
116
+
117
+ ```javascript
118
+ const knex = knexLib({
119
+ client: 'pg',
120
+ connection: {
121
+ host: '127.0.0.1',
122
+ user: 'your_username',
123
+ password: 'your_password',
124
+ database: 'your_database_name',
125
+ port: 5432
126
+ }
127
+ })
128
+ ```
129
+
130
+ For MySQL 8, use `npm install mysql2` and configure:
131
+
132
+ ```javascript
133
+ const knex = knexLib({
134
+ client: 'mysql2',
135
+ connection: {
136
+ host: '127.0.0.1',
137
+ user: 'your_username',
138
+ password: 'your_password',
139
+ database: 'your_database_name',
140
+ port: 3306
141
+ }
142
+ })
143
+ ```
144
+
145
+ The verified combinations are SQLite through better-sqlite3, PostgreSQL 16
146
+ through pg and MySQL 8 through mysql2. Review the
147
+ [backend capabilities](30-backend-capabilities.md) before switching: precision,
148
+ collation, schema evolution and transaction behavior have backend-specific
149
+ limits. Other Knex clients and server versions are unverified.
150
+
151
+ ## Method arguments and response options
152
+
153
+ Call resources through `api.resources.countries`. Methods accept operation
154
+ parameters first and optional application context second. Plain write records
155
+ go in `data`; JSON:API write documents go in `document`. Supply exactly one of
156
+ these keys. The target ID, transaction handle and response options
157
+ remain outside the record. Query selection goes in `queryParams`.
158
+
159
+ | Option | Values | Programmatic default |
160
+ | --- | --- | --- |
161
+ | `format` | `'plain'`, `'jsonapi'` | `'plain'` |
162
+ | `returning` | `'none'`, `'minimal'`, `'full'` | `'full'` |
163
+
164
+ `format` selects the output representation independently of input. Plain `get`
165
+ returns a record; plain `query` returns a collection whose `data` contains
166
+ records. JSON:API output is a document with `data`. Either output can be requested
167
+ with either input key. Record attributes never become top-level method parameters.
168
+
169
+ For POST, PUT and PATCH, `returning: 'minimal'` returns `{ type, id }` in plain
170
+ format or `{ data: { type, id } }` in JSON:API format. `returning: 'none'` returns
171
+ undefined. Full responses run the resource read/enrichment path, including its
172
+ visibility rules and selected fields. DELETE and relationship writes return
173
+ undefined. `returning` does not change read results.
174
+
175
+ You can set programmatic defaults when installing `RestApiPlugin`, override
176
+ them in a resource definition, and override those defaults for a particular
177
+ call. Each setting accepts a single value, not a per-method map or boolean.
178
+
179
+ ```javascript
180
+ await api.use(RestApiPlugin, { format: 'plain', returning: 'minimal' })
181
+ await api.addResource('countries', {
182
+ returning: 'full',
183
+ schema: { name: { type: 'string', required: true } }
184
+ })
185
+ const result = await api.resources.countries.post({
186
+ data: { name: 'Canada' },
187
+ returning: 'none'
188
+ })
189
+ // result is undefined
190
+ ```
191
+
192
+ This is an alternative configuration for a fresh API instance; do not install
193
+ the plugin or register the same resource twice in the initial script.
194
+
195
+ PUT creates a missing target or replaces a visible existing record according
196
+ to schema requirements, defaults and nullability. PATCH changes supplied fields.
197
+ Both accept the target ID as `params.id` or in the input record; conflicting
198
+ IDs are rejected. For exact payloads, relationships and pagination, see the
199
+ [API reference](../API.md).
200
+
201
+ ## HTTP endpoints
202
+
203
+ The library provides [Express](../QUICKSTART.md) and
204
+ [Fastify](23-fastify.md) connectors. Install the connector before resource
205
+ registration. Express exposes `api.http.express.router` and
206
+ `api.http.express.notFoundRouter`, which your application mounts with `app.use`.
207
+ The quickstart contains the complete setup and shutdown code.
208
+
209
+ Generated HTTP routes explicitly use JSON:API format and full write responses.
210
+ They do not inherit a resource's programmatic plain/minimal defaults. There is
211
+ no separate simplified-transport switch.
212
+
213
+ For a server exposing the countries resource at `/api`, create a country with:
214
+
215
+ ```bash
216
+ curl -i -X POST http://localhost:3000/api/countries \
217
+ -H 'Content-Type: application/vnd.api+json' \
218
+ -d '{"data":{"type":"countries","attributes":{"name":"United Kingdom","code":"GB"}}}'
219
+ ```
220
+
221
+ Successful POST returns 201 with a JSON:API document. Use its `data.id` in a
222
+ subsequent GET, for example `curl http://localhost:3000/api/countries/1` on an
223
+ otherwise empty database. Successful PUT/PATCH with a response body return 200;
224
+ DELETE returns 204 with no body. A custom endpoint selecting no write result
225
+ returns 204; it must not send a JSON body with that status. These describe normal
226
+ successful responses; application hooks can reject requests or customize responses.
227
+
228
+ ## Limits and resource configuration
229
+
230
+ Configure limits through documented plugin/resource options, rather than
231
+ mutating compiled resource metadata after registration.
232
+
233
+ | Option | Default | Purpose |
234
+ | --- | --- | --- |
235
+ | `queryDefaultLimit` | `20` | Default collection size |
236
+ | `queryMaxLimit` | `100` | Maximum requested collection size |
237
+ | `includeDepthLimit` | `3` | Maximum nested include depth |
238
+ | `enablePaginationCounts` | `true` | Counts for numbered pagination |
239
+ | `idProperty` | `'id'` | Physical primary-key column for table-backed resources |
240
+ | `normalizeId` | Built-in normalizer | Canonical resource identifiers |
241
+
242
+ Counts do not turn cursor pagination into numbered pagination. Omitted or empty
243
+ page options use the default collection cap without pagination metadata.
244
+ `sortableFields` and `defaultSort` control resource sorting; see
245
+ [pagination and ordering](09-pagination-and-sorting.md) for their contracts.
246
+
247
+ ## Logical IDs and Physical Columns
248
+
249
+ For table-backed resources, `idProperty` defines the physical primary-key column. The API surface still uses the logical resource id.
250
+
251
+ - plain writes use `data.id`
252
+ - JSON:API writes use `document.data.id`
253
+ - responses expose `id` or `data.id`
254
+ - the resource id is not part of `attributes`
255
+
256
+ ```javascript
257
+ await api.addResource('profiles', {
258
+ idProperty: 'user_id',
259
+ schema: {
260
+ id: { type: 'id', required: true, storage: { column: 'user_id' } },
261
+ displayName: { type: 'string', required: true },
262
+ loginCount: { type: 'number', defaultTo: 0 }
263
+ },
264
+ tableName: 'profiles'
265
+ });
266
+ ```
267
+
268
+ For table-backed resources, storage columns default to snake_case, so `displayName` maps to `display_name` and `loginCount` maps to `login_count`. Use `storage.column` for a per-field override, or `storage: { naming: 'exact' }` when physical column names should match logical field names exactly.
269
+
270
+ ## ID normalization
271
+
272
+ `normalizeId` lets you canonicalize resource identifiers before the library reads, writes, validates, or links records.
273
+
274
+ The default normalizer already handles the common cases:
275
+
276
+ - trims surrounding whitespace from strings
277
+ - converts finite numbers and bigints to strings
278
+ - rejects values that normalize to an empty identifier
279
+
280
+ You only need to provide `normalizeId` when your resource ids need additional canonicalization, such as uppercasing, lowercasing, or removing formatting characters.
281
+
282
+ ```javascript
283
+ await api.use(RestApiPlugin, {
284
+ normalizeId: (value) => {
285
+ if (value === null || value === undefined) return null
286
+
287
+ const normalized = String(value).trim()
288
+ return normalized ? normalized.toUpperCase() : null
289
+ }
290
+ })
291
+
292
+ await api.addResource('countries', {
293
+ // Override the plugin-wide normalizer for this one resource
294
+ normalizeId: (value) => {
295
+ if (value === null || value === undefined) return null
296
+
297
+ const normalized = String(value).trim()
298
+ return normalized ? normalized.toLowerCase() : null
299
+ },
300
+ schema: {
301
+ name: { type: 'string', required: true }
302
+ }
303
+ })
304
+ ```
305
+
306
+ Normalization is applied consistently to:
307
+
308
+ - `get`, `put`, `patch`, `delete`, `getRelated`, and `getRelationship` parent ids
309
+ - explicit resource ids in `POST`, `PUT`, and `PATCH` documents
310
+ - relationship identifiers supplied to `postRelationship`, `patchRelationship`, and `deleteRelationship`
311
+
312
+ The resource-level `normalizeId` override also applies to relationship identifiers when that resource appears as the related target.
313
+
314
+ If an id normalizes to an empty value:
315
+
316
+ - existing-resource operations behave as `not_found`
317
+ - explicit `POST` document ids are rejected as validation errors on `data.id`
318
+
319
+ For full examples of table creation, physical column naming, and migration generation, see [Knex Schema and Migrations](21-schema-and-migrations.md).
320
+
321
+ ## Transactions and database helpers
322
+
323
+ Use `api.transaction(callback, context?)` for a unit of library writes and pass
324
+ its managed handle as each operation's `transaction` parameter. Raw Knex
325
+ transactions are available for direct SQL and reads; they are not accepted as
326
+ owners of library writes. See [managed transactions](19-managed-transactions.md)
327
+ and [transaction outcomes](20-transaction-outcomes.md) before composing operations.
328
+
329
+ `api.knex.instance` exposes the supplied Knex instance and
330
+ `api.knex.capabilities` describes detected backend capabilities. Table-backed
331
+ resources provide `createKnexTable`, `introspectKnexTableSnapshot`,
332
+ `generateKnexMigration`, `generateKnexMigrationDiff`, `addKnexFields` and
333
+ `alterKnexFields`. These helpers have different applicability to canonical
334
+ storage; use the [schema and migration guide](21-schema-and-migrations.md)
335
+ for examples and limitations.
336
+
337
+ Continue with [the starting point](03-running-example.md) or return to
338
+ the [guide index](index.md).
@@ -0,0 +1,33 @@
1
+ ---
2
+ title: "Resources and relationships"
3
+ chapter: 2
4
+ chapter_label: "02"
5
+ ---
6
+
7
+ # 02. Resources and relationships
8
+
9
+ These tutorials cover resource CRUD, relationship linkage and related-record
10
+ queries. Use the same public relationship names in plain input and JSON:API
11
+ relationships; backing foreign-key columns are storage details.
12
+
13
+ Each runnable scenario starts from a fresh database unless it explicitly says
14
+ otherwise. Do not concatenate different chapters' resource declarations into
15
+ one API: several deliberately reuse the same resource names.
16
+
17
+ | Tutorial | What it demonstrates |
18
+ | --- | --- |
19
+ | [Starting point](03-running-example.md) | API, database and Express setup. |
20
+ | [Manipulating and searching tables](04-creating-and-querying.md) | CRUD, declared filters, custom search and sparse fields. |
21
+ | [Belongs-to relationships](05-belongs-to.md) | Public relationship input, linkage, includes, filtering and clearing. |
22
+ | [Has-many relationships](06-has-many.md) | Reverse membership, includes, related queries, attachment and removal. |
23
+ | [Polymorphic relationships](07-polymorphic-relationships.md) | Typed identifiers, distinct resources sharing an ID, includes and search. |
24
+ | [Many-to-many relationships](08-many-to-many.md) | Membership and ordinary pivot metadata, with canonical-storage distinctions. |
25
+ | [Pagination and ordering](09-pagination-and-sorting.md) | Default caps, numbered/cursor pages, sorting and sparse results. |
26
+ | [PUT and PATCH](10-put-and-patch.md) | Explicit replacement, omitted-value rejection and relationship-object boundaries. |
27
+ | [Relationship URLs](11-relationship-endpoints.md) | Identifier linkage versus related records, and relationship mutation endpoints. |
28
+
29
+ For query projections and their SQL ordering, use
30
+ [Query Projections](15-query-projections.md). For mandatory visibility before
31
+ counts and pagination, use [Row Policies](17-row-policies.md) or
32
+ [Autofiltering](16-autofiltering.md), according to the required policy.
33
+ See [backend capabilities](30-backend-capabilities.md) for database differences.
@@ -0,0 +1,87 @@
1
+ ---
2
+ title: "Running example"
3
+ chapter: 3
4
+ chapter_label: "03"
5
+ ---
6
+
7
+ # 03. Running example
8
+
9
+ ```javascript
10
+ import { JsonRestApi, RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from 'json-rest-api';
11
+ import knexLib from 'knex';
12
+ import util from 'util';
13
+ import express from 'express';
14
+
15
+ // Utility used throughout this guide
16
+ const inspect = (obj) => util.inspect(obj, { depth: 8 })
17
+
18
+ // Create a Knex instance connected to SQLite in-memory database
19
+ const knex = knexLib({
20
+ client: 'better-sqlite3',
21
+ connection: {
22
+ filename: ':memory:'
23
+ },
24
+ useNullAsDefault: true
25
+ });
26
+
27
+ // Create API instance
28
+ const api = new JsonRestApi({ name: 'book-catalog-api' });
29
+
30
+ // Install plugins
31
+ await api.use(RestApiPlugin);
32
+ await api.use(RestApiKnexPlugin, { knex });
33
+ await api.use(ExpressPlugin, { mountPath: '/api' }); // Links are generated under /api
34
+
35
+ /// *** ...programmatic calls here... ***
36
+
37
+ // Create the express server and add the API's routes
38
+ const app = express();
39
+ app.use(api.http.express.router);
40
+ app.use(api.http.express.notFoundRouter);
41
+
42
+ app.listen(3000, () => {
43
+ console.log('Express server started on port 3000. API available at http://localhost:3000/api');
44
+ }).on('error', (err) => {
45
+ console.error('Failed to start server:', err);
46
+ process.exit(1)
47
+ });
48
+ ```
49
+
50
+ Note that every time we provide a snippet of code, it will be assumed that
51
+
52
+ 1. The script is edited in the section `/// *** ...programmatic calls here... ***`
53
+ 2. The code is stopped with CTRL-C and then restarted.
54
+ 3. The code proposed in each snippet _replaces_ the code provided earlier.
55
+
56
+ This will ensure that each example has a fresh start.
57
+
58
+ Each example will be introduced programmatically first, and then via HTTP. The HTTP calls will be run assuming that the API calls (and any data created with them) stay. The use of the in-memory database will be assumed, which means that the data will start afresh each time.
59
+
60
+ ## Logical fields and storage columns
61
+
62
+ Resource definitions describe logical fields and relationships. Table-backed
63
+ storage maps those fields to physical columns: snake_case by default, explicit
64
+ `storage.column` overrides, or exact naming when configured. Canonical storage
65
+ uses its own allocation and descriptor mapping. A logical field name is not a
66
+ portable physical SQL column name.
67
+
68
+ For a definition such as `country_id: { type: 'id', belongsTo: 'countries',
69
+ as: 'country' }`, callers write the relationship through
70
+ `data: { country: countryId }`. Plain output contains a `country` object
71
+ with its ID; an unassigned to-one relationship is omitted from plain output.
72
+ Including the country adds its selected attributes.
73
+ JSON:API output represents linkage under `data.relationships.country` and
74
+ included records under `included`.
75
+
76
+ Hooks operate at specific processing stages. The write pipeline converts plain
77
+ input into its internal JSON:API representation before validation and storage.
78
+ Do not assume every hook receives raw columns or the original public payload.
79
+ Use the [hook guide](13-hooks-and-lifecycle.md) and the
80
+ [API reference](../API.md) for the context available at each stage.
81
+
82
+ Search schemas name the filters callers may use and map them to logical fields
83
+ or declared related-resource fields. Raw SQL remains available through Knex,
84
+ but code that uses physical tables/columns must account for the selected storage
85
+ backend and does not automatically inherit resource access policies.
86
+
87
+ ---
@@ -0,0 +1,181 @@
1
+ ---
2
+ title: "Creating and querying records"
3
+ chapter: 4
4
+ chapter_label: "04"
5
+ ---
6
+
7
+ # 04. Creating and querying records
8
+
9
+ This example uses countries without relationships. Insert the blocks below in
10
+ order into the [starting script](03-running-example.md), with a fresh
11
+ database and before starting the HTTP server.
12
+
13
+ ## Declare the record and search schemas
14
+
15
+ ```javascript
16
+ await api.addResource('countries', {
17
+ schema: {
18
+ name: { type: 'string', required: true, max: 100, search: true },
19
+ code: { type: 'string', max: 2, nullable: true, unique: true, search: true },
20
+ population: { type: 'number', nullable: true }
21
+ },
22
+ searchSchema: {
23
+ name: { type: 'string', filterOperator: '=' },
24
+ nameContains: { type: 'string', actualField: 'name', filterOperator: 'contains' },
25
+ populationRange: { type: 'array', actualField: 'population', filterOperator: 'between' },
26
+ words: {
27
+ type: 'string', oneOf: ['name', 'code'],
28
+ filterOperator: 'contains', splitBy: ' ', matchAll: true
29
+ },
30
+ nameOrCode: {
31
+ type: 'string',
32
+ applyFilter (query, input, { column }) {
33
+ query.where(function () {
34
+ this.whereRaw('LOWER(??) LIKE LOWER(?)', [column('name'), `%${input}%`])
35
+ .orWhereRaw('LOWER(??) = LOWER(?)', [column('code'), input])
36
+ })
37
+ }
38
+ }
39
+ }
40
+ })
41
+ await api.resources.countries.createKnexTable()
42
+ ```
43
+
44
+ The record schema validates stored data. `searchSchema` declares public filter
45
+ names and their meaning. An explicit search field overrides the same field
46
+ inferred from `search: true`; other inferred fields remain available. Here,
47
+ `name` explicitly uses equality and `code` remains searchable through its marker.
48
+ The record's `population` field is searchable through `populationRange` only.
49
+
50
+ Filter names need not be column names. `actualField` maps an alias to a logical
51
+ resource field. `oneOf` applies a filter across several fields. Search schemas
52
+ are an allowlist and input-validation contract, not arbitrary client-supplied SQL.
53
+
54
+ ## Create, patch, replace and delete records
55
+
56
+ ```javascript
57
+ const france = await api.resources.countries.post({
58
+ data: { name: 'France', code: 'FR', population: 68 }
59
+ })
60
+ const italy = await api.resources.countries.post({
61
+ data: { name: 'Italyy', code: 'IT', population: 59 }
62
+ })
63
+ const germany = await api.resources.countries.post({
64
+ data: { name: 'Germ', code: 'DE', population: 84 }
65
+ })
66
+ await api.resources.countries.patch({
67
+ id: germany.id, data: { name: 'Germany' }, returning: 'none'
68
+ })
69
+ const patched = await api.resources.countries.get({ id: germany.id })
70
+ const replaced = await api.resources.countries.put({
71
+ id: italy.id, data: { name: 'Italy', code: null, population: null }
72
+ })
73
+ console.log('Patched:', patched)
74
+ console.log('Replaced:', replaced)
75
+ await api.resources.countries.delete({ id: italy.id })
76
+ ```
77
+
78
+ These population values are illustrative numbers for demonstrating filters.
79
+ PATCH changes the supplied name while retaining Germany's code and population.
80
+ PUT replaces Italy's record and explicitly clears its nullable fields. A
81
+ replacement must include attributes that already have values; omitting them
82
+ rejects with a complete-replacement validation error. Use null explicitly when
83
+ the schema permits clearing a value. PUT can also create a missing target. DELETE returns
84
+ undefined. Writes always place record attributes inside `data`.
85
+
86
+ ## Query and filter
87
+
88
+ ```javascript
89
+ await api.resources.countries.post({
90
+ data: { name: 'United States', code: 'US', population: 330 }
91
+ })
92
+ await api.resources.countries.post({
93
+ data: { name: 'United Kingdom', code: 'GB', population: 67 }
94
+ })
95
+ await api.resources.countries.post({
96
+ data: { name: 'Austria', code: 'AT', population: 9 }
97
+ })
98
+ const exact = await api.resources.countries.query({
99
+ queryParams: { filters: { name: 'France' } }
100
+ })
101
+ const byCode = await api.resources.countries.query({
102
+ queryParams: { filters: { code: 'DE' } }
103
+ })
104
+ const contains = await api.resources.countries.query({
105
+ queryParams: { filters: { nameContains: 'United' }, sort: ['name'] }
106
+ })
107
+ const range = await api.resources.countries.query({
108
+ queryParams: { filters: { populationRange: [60, 90] }, sort: ['name'] }
109
+ })
110
+ console.log('Exact:', exact.data)
111
+ console.log('By code:', byCode.data)
112
+ console.log('Contains:', contains.data)
113
+ console.log('Range:', range.data)
114
+ ```
115
+
116
+ The results are France, Germany, the two United countries, and France/Germany/
117
+ United Kingdom respectively. Query returns an object with a `data` array, not a
118
+ bare array. Pagination metadata is supplied for explicit pagination requests;
119
+ it is not guaranteed on every collection. Multiple filter keys combine as
120
+ constraints. The selected operator and database collation determine string
121
+ comparison behavior; avoid assuming all databases ignore case.
122
+
123
+ ## Search multiple fields and words
124
+
125
+ ```javascript
126
+ const allWords = await api.resources.countries.query({
127
+ queryParams: { filters: { words: 'United US' } }
128
+ })
129
+ const custom = await api.resources.countries.query({
130
+ queryParams: { filters: { nameOrCode: 'at' }, sort: ['name'] }
131
+ })
132
+ console.log('All words:', allWords.data)
133
+ console.log('Custom filter:', custom.data)
134
+ ```
135
+
136
+ `words` splits on spaces and requires every term to match at least one of name
137
+ or code. `United US` matches United States: one term matches its name, the other
138
+ its code. Setting `matchAll: false` would allow any term to match.
139
+
140
+ The custom filter searches a case-folded name substring or an exact case-folded
141
+ code. It finds Austria and United States for `at`, not United Kingdom.
142
+ `applyFilter(query, input, { column, value, context, scopeName })` runs
143
+ synchronously and mutates the supplied Knex builder. `column(field)` translates
144
+ logical fields and `value(field, input)` converts values for their storage
145
+ representation. Group OR conditions so they do not bypass other filters or
146
+ access constraints. This example binds user input as values; SQL LIKE wildcard
147
+ characters in that input retain their SQL meaning. Custom SQL functions and
148
+ collations must be verified on the database you deploy.
149
+
150
+ ## Select fields and paginate
151
+
152
+ ```javascript
153
+ const page = await api.resources.countries.query({
154
+ queryParams: {
155
+ fields: { countries: 'name,code' },
156
+ sort: ['name'],
157
+ page: { number: 1, size: 2 }
158
+ }
159
+ })
160
+ console.log('Selected fields:', page.data)
161
+ console.log('Pagination:', page.meta.pagination)
162
+ ```
163
+
164
+ This returns Austria and France, with IDs, names and codes but no populations.
165
+ The numbered pagination total is five. Fieldsets select output and do not grant
166
+ access to hidden fields. See [pagination and ordering](09-pagination-and-sorting.md)
167
+ for cursors and collection limits.
168
+
169
+ ## HTTP queries
170
+
171
+ With the example server running:
172
+
173
+ ```bash
174
+ curl --globoff 'http://localhost:3000/api/countries?filter[nameContains]=United'
175
+ curl --globoff 'http://localhost:3000/api/countries?filter[words]=United%20US'
176
+ curl --globoff 'http://localhost:3000/api/countries?fields[countries]=name,code&page[number]=1&page[size]=2&sort=name'
177
+ ```
178
+
179
+ HTTP filters use `filter[...]`; programmatic queries use `queryParams.filters`.
180
+ Generated HTTP responses are JSON:API documents. For payload changes from the
181
+ older API, use the [migration guide](33-migrating-to-v2.md).