json-rest-api 1.0.29 → 2.0.0

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