json-rest-api 1.0.29 → 2.1.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 (294) 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 +733 -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 +103 -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 +3838 -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 +37 -0
  44. package/index.js +3 -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 +70 -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 +586 -418
  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 +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. 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).