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
@@ -1,1107 +0,0 @@
1
- # Creating Custom Storage Plugins for JSON REST API
2
-
3
- This guide explains how to create your own storage plugin for the JSON REST API library. Whether you want to use in-memory storage, connect to a remote API, or integrate with a NoSQL database, this guide will show you how to implement the required interface.
4
-
5
- ## Table of Contents
6
-
7
- 1. [Introduction](#introduction)
8
- 2. [The Storage Contract](#the-storage-contract)
9
- 3. [JSON:API Response Format](#jsonapi-response-format)
10
- 4. [Complete Example: In-Memory Storage Plugin](#complete-example-in-memory-storage-plugin)
11
- 5. [Complete Example: Remote API Storage Plugin](#complete-example-remote-api-storage-plugin)
12
- 6. [Advanced Topics](#advanced-topics)
13
- 7. [Testing Your Storage Plugin](#testing-your-storage-plugin)
14
- 8. [Common Pitfalls & Best Practices](#common-pitfalls--best-practices)
15
-
16
- ## Introduction
17
-
18
- Storage plugins are the bridge between the JSON REST API library and your data source. The REST API plugin handles all the HTTP routing, validation, permissions, and JSON:API formatting, while your storage plugin is responsible for actually storing and retrieving data.
19
-
20
- ### Why Create a Custom Storage Plugin?
21
-
22
- - **In-Memory Storage**: For testing, prototyping, or caching
23
- - **Remote APIs**: Proxy requests to another REST API or microservice
24
- - **NoSQL Databases**: Connect to MongoDB, DynamoDB, or other document stores
25
- - **Custom Logic**: Implement complex business rules or data transformations
26
- - **Hybrid Storage**: Combine multiple data sources
27
-
28
- ### How It Works
29
-
30
- The REST API plugin defines a contract of 8 helper methods that your storage plugin must implement. When a request comes in:
31
-
32
- 1. The REST API plugin handles the HTTP request
33
- 2. It validates the input and checks permissions
34
- 3. It calls your storage helper method with a context object
35
- 4. Your helper returns data in JSON:API format
36
- 5. The REST API plugin enriches the response and sends it back
37
-
38
- ## The Storage Contract
39
-
40
- Your storage plugin must implement these 8 helper methods by assigning them to the `helpers` object:
41
-
42
- ### 1. `dataExists`
43
- Check if a resource exists.
44
-
45
- ```javascript
46
- helpers.dataExists = async ({ scopeName, context }) => {
47
- // Parameters:
48
- // - scopeName: string - The resource type (e.g., 'articles')
49
- // - context.id: string|number - The resource ID to check
50
- // - context.schemaInfo.tableName: string - Storage identifier
51
- // - context.schemaInfo.idProperty: string - Primary key field name
52
- // - context.db: any - Database connection (if using transactions)
53
-
54
- // Returns: boolean - true if exists, false otherwise
55
- };
56
- ```
57
-
58
- ### 2. `dataGet`
59
- Retrieve a single resource with full JSON:API features.
60
-
61
- ```javascript
62
- helpers.dataGet = async ({ scopeName, context, runHooks }) => {
63
- // Parameters:
64
- // - scopeName: string - The resource type
65
- // - context.id: string|number - The resource ID
66
- // - context.queryParams.include: string[] - Related resources to include
67
- // - context.queryParams.fields: object - Sparse fieldsets
68
- // - context.schemaInfo: object - Schema information
69
-
70
- // Returns: JSON:API document with single resource
71
- // {
72
- // data: { type, id, attributes, relationships },
73
- // included: [...] // if includes requested
74
- // }
75
- };
76
- ```
77
-
78
- ### 3. `dataGetMinimal`
79
- Retrieve minimal resource data (used for permission checks).
80
-
81
- ```javascript
82
- helpers.dataGetMinimal = async ({ scopeName, context }) => {
83
- // Parameters: Same as dataGet but ignores queryParams
84
-
85
- // Returns: JSON:API resource object or null
86
- // {
87
- // type: 'articles',
88
- // id: '123',
89
- // attributes: { ... },
90
- // relationships: { ... } // only belongsTo relationships
91
- // }
92
- };
93
- ```
94
-
95
- ### 4. `dataQuery`
96
- Query multiple resources with filtering, sorting, and pagination.
97
-
98
- ```javascript
99
- helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
100
- // Parameters:
101
- // - context.queryParams.filters: object - Filter conditions
102
- // - context.queryParams.sort: string[] - Sort fields (prefix - for DESC)
103
- // - context.queryParams.page: object - Pagination (size, number/after/before)
104
- // - context.queryParams.include: string[] - Related resources
105
- // - context.queryParams.fields: object - Sparse fieldsets
106
-
107
- // Returns: JSON:API document with resource array
108
- // {
109
- // data: [...],
110
- // included: [...],
111
- // meta: { ... }, // pagination info
112
- // links: { ... } // pagination links
113
- // }
114
- };
115
- ```
116
-
117
- ### 5. `dataPost`
118
- Create a new resource.
119
-
120
- ```javascript
121
- helpers.dataPost = async ({ scopeName, context }) => {
122
- // Parameters:
123
- // - context.inputRecord: JSON:API document with new resource
124
- // - context.schemaInfo: object - Schema information
125
-
126
- // Returns: string|number - The ID of created resource
127
- };
128
- ```
129
-
130
- ### 6. `dataPut`
131
- Replace an entire resource (or create with specific ID).
132
-
133
- ```javascript
134
- helpers.dataPut = async ({ scopeName, context }) => {
135
- // Parameters:
136
- // - context.id: string|number - Resource ID
137
- // - context.inputRecord: JSON:API document with full resource
138
- // - context.isCreate: boolean - true if creating, false if updating
139
-
140
- // Returns: void (throws error if not found when updating)
141
- };
142
- ```
143
-
144
- ### 7. `dataPatch`
145
- Partially update a resource.
146
-
147
- ```javascript
148
- helpers.dataPatch = async ({ scopeName, context }) => {
149
- // Parameters:
150
- // - context.id: string|number - Resource ID
151
- // - context.inputRecord: JSON:API document with partial updates
152
-
153
- // Returns: void (throws error if not found)
154
- };
155
- ```
156
-
157
- ### 8. `dataDelete`
158
- Delete a resource.
159
-
160
- ```javascript
161
- helpers.dataDelete = async ({ scopeName, context }) => {
162
- // Parameters:
163
- // - context.id: string|number - Resource ID
164
-
165
- // Returns: { success: true } (throws error if not found)
166
- };
167
- ```
168
-
169
- ## JSON:API Response Format
170
-
171
- Your storage plugin must return data in proper JSON:API format. Here are the key structures:
172
-
173
- ### Single Resource Format
174
-
175
- ```javascript
176
- {
177
- data: {
178
- type: 'articles',
179
- id: '123',
180
- attributes: {
181
- title: 'My Article',
182
- content: 'Article content...',
183
- publishedAt: '2024-01-15T10:00:00Z'
184
- },
185
- relationships: {
186
- author: {
187
- data: { type: 'users', id: '456' }
188
- },
189
- tags: {
190
- data: [
191
- { type: 'tags', id: '1' },
192
- { type: 'tags', id: '2' }
193
- ]
194
- }
195
- }
196
- }
197
- }
198
- ```
199
-
200
- ### Collection Format
201
-
202
- ```javascript
203
- {
204
- data: [
205
- { type: 'articles', id: '1', attributes: {...}, relationships: {...} },
206
- { type: 'articles', id: '2', attributes: {...}, relationships: {...} }
207
- ],
208
- meta: {
209
- page: 1,
210
- pageSize: 20,
211
- pageCount: 5,
212
- total: 100
213
- },
214
- links: {
215
- first: '/articles?page[number]=1&page[size]=20',
216
- last: '/articles?page[number]=5&page[size]=20',
217
- next: '/articles?page[number]=2&page[size]=20'
218
- }
219
- }
220
- ```
221
-
222
- ### With Included Resources
223
-
224
- ```javascript
225
- {
226
- data: { type: 'articles', id: '123', ... },
227
- included: [
228
- {
229
- type: 'users',
230
- id: '456',
231
- attributes: {
232
- name: 'John Doe',
233
- email: 'john@example.com'
234
- }
235
- },
236
- {
237
- type: 'tags',
238
- id: '1',
239
- attributes: {
240
- name: 'Technology'
241
- }
242
- }
243
- ]
244
- }
245
- ```
246
-
247
- ## Complete Example: In-Memory Storage Plugin
248
-
249
- Here's a fully functional in-memory storage plugin:
250
-
251
- ```javascript
252
- export const InMemoryStoragePlugin = {
253
- name: 'in-memory-storage',
254
- dependencies: ['rest-api'],
255
-
256
- install({ helpers, scopes, log }) {
257
- // In-memory data store
258
- const dataStore = new Map();
259
-
260
- // Helper to get collection for a scope
261
- const getCollection = (scopeName) => {
262
- if (!dataStore.has(scopeName)) {
263
- dataStore.set(scopeName, new Map());
264
- }
265
- return dataStore.get(scopeName);
266
- };
267
-
268
- // Helper to generate IDs
269
- let nextId = 1;
270
- const generateId = () => String(nextId++);
271
-
272
- // 1. CHECK EXISTS
273
- helpers.dataExists = async ({ scopeName, context }) => {
274
- const collection = getCollection(scopeName);
275
- return collection.has(String(context.id));
276
- };
277
-
278
- // 2. GET SINGLE RESOURCE
279
- helpers.dataGet = async ({ scopeName, context }) => {
280
- const collection = getCollection(scopeName);
281
- const record = collection.get(String(context.id));
282
-
283
- if (!record) {
284
- const { RestApiResourceError } = await import('../../lib/rest-api-errors.js');
285
- throw new RestApiResourceError('Resource not found', {
286
- subtype: 'not_found',
287
- resourceType: scopeName,
288
- resourceId: context.id
289
- });
290
- }
291
-
292
- // Build JSON:API response
293
- const data = {
294
- type: scopeName,
295
- id: String(context.id),
296
- attributes: { ...record.attributes },
297
- relationships: {}
298
- };
299
-
300
- // Add relationships
301
- const scope = scopes[scopeName];
302
- const schemaRelationships = scope.vars.schemaInfo.schemaRelationships;
303
-
304
- for (const [relName, relDef] of Object.entries(schemaRelationships)) {
305
- if (relDef.type === 'belongsTo' && record.attributes[relDef.foreignKey]) {
306
- data.relationships[relName] = {
307
- data: {
308
- type: relDef.resource,
309
- id: String(record.attributes[relDef.foreignKey])
310
- }
311
- };
312
- } else if (relDef.type === 'hasMany' && record.relationships?.[relName]) {
313
- data.relationships[relName] = {
314
- data: record.relationships[relName].map(id => ({
315
- type: relDef.resource,
316
- id: String(id)
317
- }))
318
- };
319
- }
320
- }
321
-
322
- // Handle includes
323
- const included = [];
324
- if (context.queryParams.include?.length > 0) {
325
- for (const includePath of context.queryParams.include) {
326
- const relName = includePath.split('.')[0];
327
- const relationship = data.relationships[relName];
328
-
329
- if (relationship?.data) {
330
- const relData = Array.isArray(relationship.data)
331
- ? relationship.data
332
- : [relationship.data];
333
-
334
- for (const rel of relData) {
335
- const relCollection = getCollection(rel.type);
336
- const relRecord = relCollection.get(rel.id);
337
- if (relRecord) {
338
- included.push({
339
- type: rel.type,
340
- id: rel.id,
341
- attributes: { ...relRecord.attributes }
342
- });
343
- }
344
- }
345
- }
346
- }
347
- }
348
-
349
- // Apply sparse fieldsets
350
- if (context.queryParams.fields?.[scopeName]) {
351
- const fields = context.queryParams.fields[scopeName].split(',');
352
- data.attributes = Object.fromEntries(
353
- Object.entries(data.attributes).filter(([key]) => fields.includes(key))
354
- );
355
- }
356
-
357
- return {
358
- data,
359
- ...(included.length > 0 && { included })
360
- };
361
- };
362
-
363
- // 3. GET MINIMAL
364
- helpers.dataGetMinimal = async ({ scopeName, context }) => {
365
- const collection = getCollection(scopeName);
366
- const record = collection.get(String(context.id));
367
-
368
- if (!record) return null;
369
-
370
- const data = {
371
- type: scopeName,
372
- id: String(context.id),
373
- attributes: { ...record.attributes },
374
- relationships: {}
375
- };
376
-
377
- // Only include belongsTo relationships for minimal
378
- const scope = scopes[scopeName];
379
- const schemaRelationships = scope.vars.schemaInfo.schemaRelationships;
380
-
381
- for (const [relName, relDef] of Object.entries(schemaRelationships)) {
382
- if (relDef.type === 'belongsTo' && record.attributes[relDef.foreignKey]) {
383
- data.relationships[relName] = {
384
- data: {
385
- type: relDef.resource,
386
- id: String(record.attributes[relDef.foreignKey])
387
- }
388
- };
389
- }
390
- }
391
-
392
- return data;
393
- };
394
-
395
- // 4. QUERY RESOURCES
396
- helpers.dataQuery = async ({ scopeName, context }) => {
397
- const collection = getCollection(scopeName);
398
- let records = Array.from(collection.values());
399
-
400
- // Apply filters
401
- if (context.queryParams.filters) {
402
- records = records.filter(record => {
403
- return Object.entries(context.queryParams.filters).every(([field, value]) => {
404
- // Support nested field filtering (e.g., author.name)
405
- if (field.includes('.')) {
406
- // For simplicity, skip nested filters in this example
407
- return true;
408
- }
409
- return record.attributes[field] === value;
410
- });
411
- });
412
- }
413
-
414
- // Apply sorting
415
- if (context.queryParams.sort?.length > 0) {
416
- records.sort((a, b) => {
417
- for (const sortField of context.queryParams.sort) {
418
- const desc = sortField.startsWith('-');
419
- const field = desc ? sortField.substring(1) : sortField;
420
-
421
- const aVal = a.attributes[field];
422
- const bVal = b.attributes[field];
423
-
424
- if (aVal < bVal) return desc ? 1 : -1;
425
- if (aVal > bVal) return desc ? -1 : 1;
426
- }
427
- return 0;
428
- });
429
- }
430
-
431
- // Calculate pagination
432
- const page = context.queryParams.page || {};
433
- const pageSize = Math.min(page.size || 20, 100);
434
- const pageNumber = page.number || 1;
435
- const total = records.length;
436
- const pageCount = Math.ceil(total / pageSize);
437
-
438
- // Apply pagination
439
- const start = (pageNumber - 1) * pageSize;
440
- const paginatedRecords = records.slice(start, start + pageSize);
441
-
442
- // Build response
443
- const data = paginatedRecords.map((record, index) => ({
444
- type: scopeName,
445
- id: record.id,
446
- attributes: { ...record.attributes },
447
- relationships: {}
448
- }));
449
-
450
- // Add relationships to each record
451
- const scope = scopes[scopeName];
452
- const schemaRelationships = scope.vars.schemaInfo.schemaRelationships;
453
-
454
- data.forEach((item, index) => {
455
- const record = paginatedRecords[index];
456
- for (const [relName, relDef] of Object.entries(schemaRelationships)) {
457
- if (relDef.type === 'belongsTo' && record.attributes[relDef.foreignKey]) {
458
- item.relationships[relName] = {
459
- data: {
460
- type: relDef.resource,
461
- id: String(record.attributes[relDef.foreignKey])
462
- }
463
- };
464
- } else if (relDef.type === 'hasMany' && record.relationships?.[relName]) {
465
- item.relationships[relName] = {
466
- data: record.relationships[relName].map(id => ({
467
- type: relDef.resource,
468
- id: String(id)
469
- }))
470
- };
471
- }
472
- }
473
- });
474
-
475
- // Handle includes
476
- const included = [];
477
- if (context.queryParams.include?.length > 0) {
478
- const includedIds = new Set();
479
-
480
- for (const item of data) {
481
- for (const includePath of context.queryParams.include) {
482
- const relName = includePath.split('.')[0];
483
- const relationship = item.relationships[relName];
484
-
485
- if (relationship?.data) {
486
- const relData = Array.isArray(relationship.data)
487
- ? relationship.data
488
- : [relationship.data];
489
-
490
- for (const rel of relData) {
491
- const key = `${rel.type}:${rel.id}`;
492
- if (!includedIds.has(key)) {
493
- includedIds.add(key);
494
- const relCollection = getCollection(rel.type);
495
- const relRecord = relCollection.get(rel.id);
496
- if (relRecord) {
497
- included.push({
498
- type: rel.type,
499
- id: rel.id,
500
- attributes: { ...relRecord.attributes }
501
- });
502
- }
503
- }
504
- }
505
- }
506
- }
507
- }
508
- }
509
-
510
- // Build pagination links
511
- const baseUrl = `/${scopeName}`;
512
- const queryString = new URLSearchParams();
513
- if (page.size) queryString.set('page[size]', pageSize);
514
-
515
- const links = {
516
- first: `${baseUrl}?${queryString}&page[number]=1`,
517
- last: `${baseUrl}?${queryString}&page[number]=${pageCount}`,
518
- };
519
-
520
- if (pageNumber > 1) {
521
- links.prev = `${baseUrl}?${queryString}&page[number]=${pageNumber - 1}`;
522
- }
523
- if (pageNumber < pageCount) {
524
- links.next = `${baseUrl}?${queryString}&page[number]=${pageNumber + 1}`;
525
- }
526
-
527
- return {
528
- data,
529
- ...(included.length > 0 && { included }),
530
- meta: {
531
- page: pageNumber,
532
- pageSize,
533
- pageCount,
534
- total
535
- },
536
- links
537
- };
538
- };
539
-
540
- // 5. CREATE RESOURCE
541
- helpers.dataPost = async ({ scopeName, context }) => {
542
- const collection = getCollection(scopeName);
543
- const id = generateId();
544
-
545
- const record = {
546
- id,
547
- attributes: { ...context.inputRecord.data.attributes },
548
- relationships: {}
549
- };
550
-
551
- // Extract belongsTo foreign keys from relationships
552
- if (context.inputRecord.data.relationships) {
553
- const scope = scopes[scopeName];
554
- const schemaRelationships = scope.vars.schemaInfo.schemaRelationships;
555
-
556
- for (const [relName, relData] of Object.entries(context.inputRecord.data.relationships)) {
557
- const relDef = schemaRelationships[relName];
558
- if (relDef?.type === 'belongsTo' && relData.data) {
559
- record.attributes[relDef.foreignKey] = relData.data.id;
560
- } else if (relDef?.type === 'hasMany' && relData.data) {
561
- record.relationships[relName] = relData.data.map(item => item.id);
562
- }
563
- }
564
- }
565
-
566
- collection.set(id, record);
567
- return id;
568
- };
569
-
570
- // 6. REPLACE RESOURCE
571
- helpers.dataPut = async ({ scopeName, context }) => {
572
- const collection = getCollection(scopeName);
573
- const id = String(context.id);
574
-
575
- if (!context.isCreate && !collection.has(id)) {
576
- const { RestApiResourceError } = await import('../../lib/rest-api-errors.js');
577
- throw new RestApiResourceError('Resource not found', {
578
- subtype: 'not_found',
579
- resourceType: scopeName,
580
- resourceId: context.id
581
- });
582
- }
583
-
584
- const record = {
585
- id,
586
- attributes: { ...context.inputRecord.data.attributes },
587
- relationships: {}
588
- };
589
-
590
- // Extract belongsTo foreign keys
591
- if (context.inputRecord.data.relationships) {
592
- const scope = scopes[scopeName];
593
- const schemaRelationships = scope.vars.schemaInfo.schemaRelationships;
594
-
595
- for (const [relName, relData] of Object.entries(context.inputRecord.data.relationships)) {
596
- const relDef = schemaRelationships[relName];
597
- if (relDef?.type === 'belongsTo' && relData.data) {
598
- record.attributes[relDef.foreignKey] = relData.data.id;
599
- } else if (relDef?.type === 'hasMany' && relData.data) {
600
- record.relationships[relName] = relData.data.map(item => item.id);
601
- }
602
- }
603
- }
604
-
605
- collection.set(id, record);
606
- };
607
-
608
- // 7. UPDATE RESOURCE
609
- helpers.dataPatch = async ({ scopeName, context }) => {
610
- const collection = getCollection(scopeName);
611
- const id = String(context.id);
612
- const existing = collection.get(id);
613
-
614
- if (!existing) {
615
- const { RestApiResourceError } = await import('../../lib/rest-api-errors.js');
616
- throw new RestApiResourceError('Resource not found', {
617
- subtype: 'not_found',
618
- resourceType: scopeName,
619
- resourceId: context.id
620
- });
621
- }
622
-
623
- // Merge attributes
624
- if (context.inputRecord.data.attributes) {
625
- Object.assign(existing.attributes, context.inputRecord.data.attributes);
626
- }
627
-
628
- // Update relationships
629
- if (context.inputRecord.data.relationships) {
630
- const scope = scopes[scopeName];
631
- const schemaRelationships = scope.vars.schemaInfo.schemaRelationships;
632
-
633
- for (const [relName, relData] of Object.entries(context.inputRecord.data.relationships)) {
634
- const relDef = schemaRelationships[relName];
635
- if (relDef?.type === 'belongsTo') {
636
- if (relData.data === null) {
637
- delete existing.attributes[relDef.foreignKey];
638
- } else if (relData.data) {
639
- existing.attributes[relDef.foreignKey] = relData.data.id;
640
- }
641
- } else if (relDef?.type === 'hasMany' && relData.data) {
642
- existing.relationships[relName] = relData.data.map(item => item.id);
643
- }
644
- }
645
- }
646
- };
647
-
648
- // 8. DELETE RESOURCE
649
- helpers.dataDelete = async ({ scopeName, context }) => {
650
- const collection = getCollection(scopeName);
651
- const id = String(context.id);
652
-
653
- if (!collection.has(id)) {
654
- const { RestApiResourceError } = await import('../../lib/rest-api-errors.js');
655
- throw new RestApiResourceError('Resource not found', {
656
- subtype: 'not_found',
657
- resourceType: scopeName,
658
- resourceId: context.id
659
- });
660
- }
661
-
662
- collection.delete(id);
663
- return { success: true };
664
- };
665
-
666
- log.info('InMemoryStoragePlugin installed - data stored in memory');
667
- }
668
- };
669
- ```
670
-
671
- ## Complete Example: Remote API Storage Plugin
672
-
673
- Here's a storage plugin that proxies requests to a remote API:
674
-
675
- ```javascript
676
- export const RemoteApiStoragePlugin = {
677
- name: 'remote-api-storage',
678
- dependencies: ['rest-api'],
679
-
680
- install({ helpers, vars, pluginOptions, log }) {
681
- const baseUrl = pluginOptions.baseUrl || 'https://api.example.com';
682
- const headers = {
683
- 'Content-Type': 'application/vnd.api+json',
684
- 'Accept': 'application/vnd.api+json',
685
- // Add authentication if needed
686
- ...(pluginOptions.token && { 'Authorization': `Bearer ${pluginOptions.token}` }),
687
- ...(pluginOptions.headers || {})
688
- };
689
-
690
- // Helper to make fetch requests
691
- const fetchApi = async (path, options = {}) => {
692
- const url = `${baseUrl}${path}`;
693
- const response = await fetch(url, {
694
- ...options,
695
- headers: {
696
- ...headers,
697
- ...options.headers
698
- }
699
- });
700
-
701
- if (!response.ok) {
702
- const error = await response.json().catch(() => ({ errors: [{ title: 'Request failed' }] }));
703
- const { RestApiResourceError } = await import('../../lib/rest-api-errors.js');
704
- throw new RestApiResourceError(
705
- error.errors?.[0]?.title || 'Remote API error',
706
- {
707
- subtype: response.status === 404 ? 'not_found' : 'remote_error',
708
- statusCode: response.status,
709
- errors: error.errors
710
- }
711
- );
712
- }
713
-
714
- return response.json();
715
- };
716
-
717
- // 1. CHECK EXISTS
718
- helpers.dataExists = async ({ scopeName, context }) => {
719
- try {
720
- await fetchApi(`/${scopeName}/${context.id}`, { method: 'HEAD' });
721
- return true;
722
- } catch (error) {
723
- if (error.statusCode === 404) return false;
724
- throw error;
725
- }
726
- };
727
-
728
- // 2. GET SINGLE RESOURCE
729
- helpers.dataGet = async ({ scopeName, context }) => {
730
- const queryParams = new URLSearchParams();
731
-
732
- // Add include parameter
733
- if (context.queryParams.include?.length > 0) {
734
- queryParams.set('include', context.queryParams.include.join(','));
735
- }
736
-
737
- // Add sparse fieldsets
738
- if (context.queryParams.fields) {
739
- for (const [type, fields] of Object.entries(context.queryParams.fields)) {
740
- queryParams.set(`fields[${type}]`, fields);
741
- }
742
- }
743
-
744
- const query = queryParams.toString();
745
- const path = `/${scopeName}/${context.id}${query ? `?${query}` : ''}`;
746
-
747
- return await fetchApi(path);
748
- };
749
-
750
- // 3. GET MINIMAL
751
- helpers.dataGetMinimal = async ({ scopeName, context }) => {
752
- const response = await fetchApi(`/${scopeName}/${context.id}`);
753
- return response.data;
754
- };
755
-
756
- // 4. QUERY RESOURCES
757
- helpers.dataQuery = async ({ scopeName, context }) => {
758
- const queryParams = new URLSearchParams();
759
-
760
- // Add filters
761
- if (context.queryParams.filters) {
762
- for (const [field, value] of Object.entries(context.queryParams.filters)) {
763
- queryParams.set(`filter[${field}]`, value);
764
- }
765
- }
766
-
767
- // Add sorting
768
- if (context.queryParams.sort?.length > 0) {
769
- queryParams.set('sort', context.queryParams.sort.join(','));
770
- }
771
-
772
- // Add pagination
773
- if (context.queryParams.page) {
774
- for (const [key, value] of Object.entries(context.queryParams.page)) {
775
- queryParams.set(`page[${key}]`, value);
776
- }
777
- }
778
-
779
- // Add includes
780
- if (context.queryParams.include?.length > 0) {
781
- queryParams.set('include', context.queryParams.include.join(','));
782
- }
783
-
784
- // Add sparse fieldsets
785
- if (context.queryParams.fields) {
786
- for (const [type, fields] of Object.entries(context.queryParams.fields)) {
787
- queryParams.set(`fields[${type}]`, fields);
788
- }
789
- }
790
-
791
- const query = queryParams.toString();
792
- const path = `/${scopeName}${query ? `?${query}` : ''}`;
793
-
794
- const response = await fetchApi(path);
795
-
796
- // Ensure proper structure
797
- return {
798
- data: response.data || [],
799
- included: response.included,
800
- meta: response.meta,
801
- links: response.links
802
- };
803
- };
804
-
805
- // 5. CREATE RESOURCE
806
- helpers.dataPost = async ({ scopeName, context }) => {
807
- const response = await fetchApi(`/${scopeName}`, {
808
- method: 'POST',
809
- body: JSON.stringify(context.inputRecord)
810
- });
811
-
812
- return response.data.id;
813
- };
814
-
815
- // 6. REPLACE RESOURCE
816
- helpers.dataPut = async ({ scopeName, context }) => {
817
- await fetchApi(`/${scopeName}/${context.id}`, {
818
- method: 'PUT',
819
- body: JSON.stringify(context.inputRecord)
820
- });
821
- };
822
-
823
- // 7. UPDATE RESOURCE
824
- helpers.dataPatch = async ({ scopeName, context }) => {
825
- await fetchApi(`/${scopeName}/${context.id}`, {
826
- method: 'PATCH',
827
- body: JSON.stringify(context.inputRecord)
828
- });
829
- };
830
-
831
- // 8. DELETE RESOURCE
832
- helpers.dataDelete = async ({ scopeName, context }) => {
833
- await fetchApi(`/${scopeName}/${context.id}`, {
834
- method: 'DELETE'
835
- });
836
-
837
- return { success: true };
838
- };
839
-
840
- log.info(`RemoteApiStoragePlugin installed - proxying to ${baseUrl}`);
841
- }
842
- };
843
- ```
844
-
845
- ## Advanced Topics
846
-
847
- ### Transaction Support
848
-
849
- If your storage supports transactions, the `context.db` parameter will automatically contain the transaction when one is active:
850
-
851
- ```javascript
852
- helpers.dataPost = async ({ scopeName, context }) => {
853
- // context.db is automatically the transaction if one is active,
854
- // or the base connection if not in a transaction
855
- const db = context.db || defaultConnection;
856
-
857
- // For storage that supports transactions:
858
- if (context.transaction) {
859
- // We're in a transaction - ensure all operations use it
860
- await db.insert(scopeName, record);
861
- } else {
862
- // No transaction - use regular connection
863
- await db.insert(scopeName, record);
864
- }
865
- };
866
- ```
867
-
868
- ### Sparse Fieldsets
869
-
870
- When `context.queryParams.fields` is provided, only return the requested fields:
871
-
872
- ```javascript
873
- // Example: fields[articles]=title,summary
874
- // Can be a string or array depending on how it was parsed
875
- const requestedFields = context.queryParams.fields[scopeName];
876
- if (requestedFields) {
877
- const fields = Array.isArray(requestedFields)
878
- ? requestedFields
879
- : requestedFields.split(',');
880
-
881
- // Filter attributes to only include requested fields
882
- data.attributes = Object.fromEntries(
883
- Object.entries(data.attributes).filter(([key]) => fields.includes(key))
884
- );
885
-
886
- // Handle nested field requests like fields[articles]=title,author.name
887
- // The REST API plugin will handle nested field filtering for included resources
888
- }
889
- ```
890
-
891
- ### Computed Fields and Dependencies
892
-
893
- The REST API plugin handles computed fields, but you may need to ensure dependency fields are included:
894
-
895
- ```javascript
896
- // context.computedDependencies tells you which fields are needed for computations
897
- // Always include these fields even if not explicitly requested
898
- ```
899
-
900
- ### Search and Filter Implementation
901
-
902
- For complex filtering, you'll need to parse the search schema:
903
-
904
- ```javascript
905
- // context.schemaInfo.searchSchema defines what can be filtered
906
- // context.queryParams.filters contains the actual filter values
907
- ```
908
-
909
- ### Error Handling
910
-
911
- Always use the proper error classes:
912
-
913
- ```javascript
914
- import { RestApiResourceError, RestApiValidationError } from '../../lib/rest-api-errors.js';
915
-
916
- // For not found errors
917
- throw new RestApiResourceError('Resource not found', {
918
- subtype: 'not_found',
919
- resourceType: scopeName,
920
- resourceId: id
921
- });
922
-
923
- // For validation errors
924
- throw new RestApiValidationError('Invalid filter value', {
925
- fields: ['filters.status'],
926
- violations: [{
927
- field: 'filters.status',
928
- rule: 'invalid_value',
929
- message: 'Status must be one of: draft, published'
930
- }]
931
- });
932
- ```
933
-
934
- ## Testing Your Storage Plugin
935
-
936
- ### Using the Test Suite
937
-
938
- The JSON REST API test suite can be adapted for your storage plugin:
939
-
940
- ```javascript
941
- import { createBasicApi } from './tests/fixtures/api-configs.js';
942
- import { YourStoragePlugin } from './your-storage-plugin.js';
943
-
944
- describe('Your Storage Plugin', () => {
945
- let api;
946
-
947
- before(async () => {
948
- // Create API with your storage instead of Knex
949
- api = await createApi({
950
- plugins: [
951
- [RestApiPlugin, { /* options */ }],
952
- [YourStoragePlugin, { /* options */ }],
953
- // ... other plugins
954
- ]
955
- });
956
- });
957
-
958
- it('should create and retrieve a resource', async () => {
959
- const result = await api.resources.articles.post({
960
- inputRecord: {
961
- data: {
962
- type: 'articles',
963
- attributes: {
964
- title: 'Test Article'
965
- }
966
- }
967
- }
968
- });
969
-
970
- const article = await api.resources.articles.get({ id: result.id });
971
- expect(article.data.attributes.title).to.equal('Test Article');
972
- });
973
- });
974
- ```
975
-
976
- ### Storage-Specific Tests
977
-
978
- Test edge cases specific to your storage:
979
-
980
- ```javascript
981
- describe('Edge Cases', () => {
982
- it('should handle concurrent writes', async () => {
983
- // Test your storage's concurrency handling
984
- });
985
-
986
- it('should handle large datasets', async () => {
987
- // Test pagination with many records
988
- });
989
-
990
- it('should handle network failures gracefully', async () => {
991
- // For remote storage, test connection issues
992
- });
993
- });
994
- ```
995
-
996
- ## Common Pitfalls & Best Practices
997
-
998
- ### 1. ID Type Conversion
999
-
1000
- JSON:API requires IDs to be strings, but your storage might use numbers:
1001
-
1002
- ```javascript
1003
- // Always convert IDs to strings in responses
1004
- data.id = String(record.id);
1005
-
1006
- // Accept both strings and numbers in inputs
1007
- const id = String(context.id);
1008
- ```
1009
-
1010
- ### 2. Relationship Format
1011
-
1012
- Relationships must follow the JSON:API format exactly:
1013
-
1014
- ```javascript
1015
- // Correct - single relationship
1016
- relationships: {
1017
- author: {
1018
- data: { type: 'users', id: '123' }
1019
- }
1020
- }
1021
-
1022
- // Correct - to-many relationship
1023
- relationships: {
1024
- tags: {
1025
- data: [
1026
- { type: 'tags', id: '1' },
1027
- { type: 'tags', id: '2' }
1028
- ]
1029
- }
1030
- }
1031
-
1032
- // Correct - empty relationship
1033
- relationships: {
1034
- author: {
1035
- data: null
1036
- }
1037
- }
1038
- ```
1039
-
1040
- ### 3. Error Response Format
1041
-
1042
- Errors should include proper subtypes:
1043
-
1044
- ```javascript
1045
- // Use these standard subtypes
1046
- 'not_found' - Resource doesn't exist
1047
- 'validation_error' - Input validation failed
1048
- 'permission_denied' - Insufficient permissions
1049
- 'conflict' - Resource conflict (e.g., duplicate)
1050
- ```
1051
-
1052
- ### 4. Pagination Meta
1053
-
1054
- Always include pagination metadata for queries:
1055
-
1056
- ```javascript
1057
- meta: {
1058
- page: 1, // Current page
1059
- pageSize: 20, // Items per page
1060
- pageCount: 5, // Total pages
1061
- total: 100 // Total items
1062
- }
1063
- ```
1064
-
1065
- ### 5. Include Deduplication
1066
-
1067
- When returning included resources, avoid duplicates:
1068
-
1069
- ```javascript
1070
- const includedMap = new Map();
1071
- // Use type:id as key to ensure uniqueness
1072
- includedMap.set(`${type}:${id}`, resource);
1073
- const included = Array.from(includedMap.values());
1074
- ```
1075
-
1076
- ### 6. Performance Considerations
1077
-
1078
- - Cache frequently accessed data
1079
- - Implement efficient filtering at the storage level
1080
- - Use bulk operations where possible
1081
- - Consider implementing cursor-based pagination for large datasets
1082
-
1083
- ### 7. Schema Information
1084
-
1085
- Use the schema information provided in context:
1086
-
1087
- ```javascript
1088
- // Available in context.schemaInfo:
1089
- - tableName: Storage identifier for the resource
1090
- - idProperty: Primary key field name (might not be 'id')
1091
- - schema: Full schema definition
1092
- - schemaRelationships: Relationship definitions
1093
- - searchSchema: Filterable fields and their rules
1094
- ```
1095
-
1096
- ## Conclusion
1097
-
1098
- Creating a custom storage plugin gives you complete control over how data is stored and retrieved while leveraging all the features of the JSON REST API library. The key is to properly implement the 8 required helpers and ensure all responses follow the JSON:API specification.
1099
-
1100
- Remember:
1101
- - Start with the in-memory example and adapt it to your needs
1102
- - Always return proper JSON:API formatted responses
1103
- - Use the provided error classes for consistency
1104
- - Test thoroughly with the existing test suite
1105
- - Refer to the Knex plugin source code for complex implementations
1106
-
1107
- Happy coding!