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,163 +0,0 @@
1
- import { describe, it, before, beforeEach, after } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import knexLib from 'knex'
4
- import { Api } from 'hooked-api'
5
-
6
- import { RestApiPlugin, RestApiAnyapiKnexPlugin } from '../index.js'
7
- import { ensureAnyApiSchema } from '../plugins/core/lib/anyapi/schema-utils.js'
8
- import { storageMode } from './helpers/storage-mode.js'
9
- import {
10
- createJsonApiDocument,
11
- createRelationship,
12
- resourceIdentifier,
13
- } from './helpers/test-utils.js'
14
-
15
- const maybeDescribe = storageMode.isAnyApi() ? describe : describe.skip
16
-
17
- maybeDescribe('AnyAPI custom idProperty', () => {
18
- const knex = knexLib({
19
- client: 'better-sqlite3',
20
- connection: { filename: ':memory:' },
21
- useNullAsDefault: true,
22
- })
23
-
24
- let api
25
-
26
- before(async () => {
27
- await ensureAnyApiSchema(knex)
28
-
29
- api = new Api({ name: 'anyapi-custom-idproperty-test', log: { level: 'warn' } })
30
- await api.use(RestApiPlugin, {
31
- simplifiedApi: false,
32
- simplifiedTransport: false,
33
- returnRecordApi: {
34
- post: 'full',
35
- put: 'full',
36
- patch: 'full',
37
- },
38
- sortableFields: ['id', 'owner_id', 'item_id', 'code', 'name', 'category'],
39
- queryDefaultLimit: 20,
40
- queryMaxLimit: 100,
41
- })
42
- await api.use(RestApiAnyapiKnexPlugin, { knex })
43
-
44
- await api.addResource('owners', {
45
- schema: {
46
- owner_id: { type: 'id' },
47
- name: { type: 'string', required: true },
48
- },
49
- idProperty: 'owner_id',
50
- tableName: 'custom_id_owners',
51
- })
52
- await api.resources.owners.createKnexTable()
53
-
54
- await api.addResource('items', {
55
- schema: {
56
- item_id: { type: 'id' },
57
- code: { type: 'string', required: true, search: true },
58
- name: { type: 'string', required: true },
59
- category: { type: 'string', required: true },
60
- owner_id: { type: 'id', belongsTo: 'owners', as: 'owner' },
61
- },
62
- idProperty: 'item_id',
63
- tableName: 'custom_id_items',
64
- })
65
- await api.resources.items.createKnexTable()
66
- })
67
-
68
- after(async () => {
69
- await api?.release()
70
- await knex.destroy()
71
- })
72
-
73
- beforeEach(async () => {
74
- await knex('any_links').delete()
75
- await knex('any_records').delete()
76
- })
77
-
78
- it('uses logical ids for post, get, query, sparse fields, cursors, and includes', async () => {
79
- const owner = await api.resources.owners.post({
80
- inputRecord: createJsonApiDocument('owners', { name: 'Owner One' }),
81
- simplified: false,
82
- })
83
-
84
- const itemInputs = [
85
- { code: 'A001', name: 'Apple', category: 'Fruit' },
86
- { code: 'A002', name: 'Apricot', category: 'Fruit' },
87
- { code: 'B001', name: 'Broccoli', category: 'Vegetable' },
88
- { code: 'C001', name: 'Carrot', category: 'Vegetable' },
89
- ]
90
-
91
- const createdItems = []
92
- for (const item of itemInputs) {
93
- const created = await api.resources.items.post({
94
- inputRecord: createJsonApiDocument(
95
- 'items',
96
- item,
97
- { owner: createRelationship(resourceIdentifier('owners', owner.data.id)) }
98
- ),
99
- simplified: false,
100
- })
101
- createdItems.push(created)
102
- }
103
-
104
- const fetched = await api.resources.items.get({
105
- id: createdItems[0].data.id,
106
- queryParams: {
107
- include: ['owner'],
108
- fields: {
109
- items: 'item_id,code,name,category',
110
- owners: 'owner_id,name',
111
- },
112
- },
113
- simplified: false,
114
- })
115
-
116
- assert.equal(fetched.data.id, createdItems[0].data.id)
117
- assert.equal(fetched.data.attributes.code, 'A001')
118
- assert.equal(fetched.data.attributes.name, 'Apple')
119
- assert.equal(fetched.data.attributes.item_id, undefined)
120
- assert.equal(fetched.included?.[0]?.type, 'owners')
121
- assert.equal(fetched.included?.[0]?.id, owner.data.id)
122
- assert.equal(fetched.included?.[0]?.attributes.owner_id, undefined)
123
-
124
- const firstPage = await api.resources.items.query({
125
- queryParams: {
126
- include: ['owner'],
127
- fields: {
128
- items: 'item_id,code,name,category',
129
- owners: 'owner_id,name',
130
- },
131
- sort: ['category', 'name'],
132
- page: { size: 2 },
133
- },
134
- simplified: false,
135
- })
136
-
137
- assert.equal(firstPage.data.length, 2)
138
- assert(firstPage.meta.pagination.cursor.next, 'first page should include a next cursor')
139
- assert(firstPage.data.every((item) => item.attributes.item_id === undefined))
140
- assert(firstPage.included?.some((entry) => entry.type === 'owners' && entry.id === owner.data.id))
141
-
142
- const secondPage = await api.resources.items.query({
143
- queryParams: {
144
- include: ['owner'],
145
- fields: {
146
- items: 'item_id,code,name,category',
147
- owners: 'owner_id,name',
148
- },
149
- sort: ['category', 'name'],
150
- page: {
151
- size: 2,
152
- after: firstPage.meta.pagination.cursor.next,
153
- },
154
- },
155
- simplified: false,
156
- })
157
-
158
- const firstIds = firstPage.data.map((item) => item.id)
159
- const secondIds = secondPage.data.map((item) => item.id)
160
- assert.equal(secondPage.data.length, 2)
161
- assert(!secondIds.some((id) => firstIds.includes(id)), 'cursor page should not repeat records')
162
- })
163
- })
@@ -1,463 +0,0 @@
1
- import { describe, it, before, after, beforeEach } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import knexLib from 'knex'
4
- import {
5
- validateJsonApiStructure,
6
- cleanTables,
7
- createJsonApiDocument,
8
- assertResourceAttributes,
9
- createRelationship,
10
- resourceIdentifier
11
- } from './helpers/test-utils.js'
12
- import { storageMode } from './helpers/storage-mode.js'
13
- import { createAutoFilterApi } from './fixtures/api-configs.js'
14
-
15
- const knex = knexLib({
16
- client: 'better-sqlite3',
17
- connection: {
18
- filename: ':memory:'
19
- },
20
- useNullAsDefault: true
21
- })
22
-
23
- let api
24
-
25
- const scopedContext = (workspaceId, userId, projectId) => ({
26
- scopeValues: {
27
- workspaceId,
28
- userId,
29
- ...(projectId !== undefined ? { projectId } : {})
30
- }
31
- })
32
-
33
- const moveTaskOutOfScope = async ({ taskId, workspaceId, userId }) => {
34
- if (storageMode.isAnyApi()) {
35
- const descriptor = await api.anyapi.registry.getDescriptor(api.anyapi.tenantId, 'tasks')
36
- await knex('any_records')
37
- .where({
38
- id: Number(taskId),
39
- tenant_id: api.anyapi.tenantId,
40
- resource: 'tasks'
41
- })
42
- .update({
43
- [descriptor.fields.workspace_id.slot]: workspaceId,
44
- [descriptor.fields.user_id.slot]: userId
45
- })
46
- return
47
- }
48
-
49
- await knex('autofilter_tasks')
50
- .where({ id: Number(taskId) })
51
- .update({
52
- workspace_id: workspaceId,
53
- user_id: userId
54
- })
55
- }
56
-
57
- describe('AutoFilter Plugin', () => {
58
- before(async () => {
59
- api = await createAutoFilterApi(knex)
60
- })
61
-
62
- after(async () => {
63
- await knex.destroy()
64
- })
65
-
66
- beforeEach(async () => {
67
- await cleanTables(knex, [
68
- 'autofilter_workspace_reports',
69
- 'autofilter_user_notes',
70
- 'autofilter_projects',
71
- 'autofilter_tasks',
72
- 'autofilter_optional_tasks',
73
- 'autofilter_system_settings'
74
- ])
75
- })
76
-
77
- it('stamps workspace-scoped records and filters collections by workspace', async () => {
78
- const reportA = await api.resources.workspace_reports.post({
79
- inputRecord: createJsonApiDocument('workspace_reports', {
80
- title: 'Workspace A report'
81
- }),
82
- simplified: false
83
- }, scopedContext('workspace-a', 101))
84
-
85
- await api.resources.workspace_reports.post({
86
- inputRecord: createJsonApiDocument('workspace_reports', {
87
- title: 'Workspace B report'
88
- }),
89
- simplified: false
90
- }, scopedContext('workspace-b', 202))
91
-
92
- validateJsonApiStructure(reportA)
93
- assertResourceAttributes(reportA.data, {
94
- title: 'Workspace A report',
95
- workspace_id: 'workspace-a'
96
- })
97
-
98
- const queryA = await api.resources.workspace_reports.query({
99
- simplified: false
100
- }, scopedContext('workspace-a', 999))
101
-
102
- validateJsonApiStructure(queryA, true)
103
- assert.equal(queryA.data.length, 1)
104
- assert.equal(queryA.data[0].attributes.title, 'Workspace A report')
105
- assert.equal(queryA.data[0].attributes.workspace_id, 'workspace-a')
106
- })
107
-
108
- it('stamps user-scoped records and filters collections by user', async () => {
109
- const noteA = await api.resources.user_notes.post({
110
- inputRecord: createJsonApiDocument('user_notes', {
111
- body: 'User 101 note'
112
- }),
113
- simplified: false
114
- }, scopedContext('workspace-a', 101))
115
-
116
- await api.resources.user_notes.post({
117
- inputRecord: createJsonApiDocument('user_notes', {
118
- body: 'User 202 note'
119
- }),
120
- simplified: false
121
- }, scopedContext('workspace-a', 202))
122
-
123
- validateJsonApiStructure(noteA)
124
- assertResourceAttributes(noteA.data, {
125
- body: 'User 101 note',
126
- user_id: 101
127
- })
128
-
129
- const queryA = await api.resources.user_notes.query({
130
- simplified: false
131
- }, scopedContext('workspace-b', 101))
132
-
133
- validateJsonApiStructure(queryA, true)
134
- assert.equal(queryA.data.length, 1)
135
- assert.equal(queryA.data[0].attributes.body, 'User 101 note')
136
- assert.equal(queryA.data[0].attributes.user_id, 101)
137
- })
138
-
139
- it('applies composite workspace+user scoping', async () => {
140
- const ownProject = await api.resources.projects.post({
141
- inputRecord: createJsonApiDocument('projects', {
142
- name: 'Workspace A / User 101'
143
- }),
144
- simplified: false
145
- }, scopedContext('workspace-a', 101))
146
-
147
- await api.resources.projects.post({
148
- inputRecord: createJsonApiDocument('projects', {
149
- name: 'Workspace A / User 202'
150
- }),
151
- simplified: false
152
- }, scopedContext('workspace-a', 202))
153
-
154
- await api.resources.projects.post({
155
- inputRecord: createJsonApiDocument('projects', {
156
- name: 'Workspace B / User 101'
157
- }),
158
- simplified: false
159
- }, scopedContext('workspace-b', 101))
160
-
161
- validateJsonApiStructure(ownProject)
162
- assertResourceAttributes(ownProject.data, {
163
- name: 'Workspace A / User 101',
164
- workspace_id: 'workspace-a',
165
- user_id: 101
166
- })
167
-
168
- const scopedQuery = await api.resources.projects.query({
169
- simplified: false
170
- }, scopedContext('workspace-a', 101))
171
-
172
- validateJsonApiStructure(scopedQuery, true)
173
- assert.equal(scopedQuery.data.length, 1)
174
- assert.equal(scopedQuery.data[0].attributes.name, 'Workspace A / User 101')
175
- })
176
-
177
- it('treats public resources as unscoped', async () => {
178
- const created = await api.resources.system_settings.post({
179
- inputRecord: createJsonApiDocument('system_settings', {
180
- key: 'app.version',
181
- value: '1.0.0'
182
- }),
183
- simplified: false
184
- })
185
-
186
- validateJsonApiStructure(created)
187
-
188
- const anonymousQuery = await api.resources.system_settings.query({
189
- simplified: false
190
- })
191
- validateJsonApiStructure(anonymousQuery, true)
192
- assert.equal(anonymousQuery.data.length, 1)
193
-
194
- const scopedQuery = await api.resources.system_settings.query({
195
- simplified: false
196
- }, scopedContext('workspace-a', 101))
197
- validateJsonApiStructure(scopedQuery, true)
198
- assert.equal(scopedQuery.data.length, 1)
199
- })
200
-
201
- it('scopes single-record lookups so out-of-scope records are not found', async () => {
202
- const project = await api.resources.projects.post({
203
- inputRecord: createJsonApiDocument('projects', {
204
- name: 'Scoped Project'
205
- }),
206
- simplified: false
207
- }, scopedContext('workspace-a', 101))
208
-
209
- await assert.rejects(
210
- async () => {
211
- await api.resources.projects.get({
212
- id: project.data.id,
213
- simplified: false
214
- }, scopedContext('workspace-a', 202))
215
- },
216
- (error) => error.code === 'REST_API_RESOURCE',
217
- 'Out-of-scope records should behave as not found'
218
- )
219
- })
220
-
221
- it('stamps scoped fields on PUT and rejects inconsistent scoped updates', async () => {
222
- const project = await api.resources.projects.post({
223
- inputRecord: createJsonApiDocument('projects', {
224
- name: 'Original Project',
225
- description: 'Original Description'
226
- }),
227
- simplified: false
228
- }, scopedContext('workspace-a', 101))
229
-
230
- await api.resources.projects.put({
231
- inputRecord: {
232
- data: {
233
- type: 'projects',
234
- id: project.data.id,
235
- attributes: {
236
- name: 'Updated Project',
237
- description: 'Updated Description',
238
- status: 'active'
239
- }
240
- }
241
- },
242
- simplified: false
243
- }, scopedContext('workspace-a', 101))
244
-
245
- const afterPut = await api.resources.projects.get({
246
- id: project.data.id,
247
- simplified: false
248
- }, scopedContext('workspace-a', 101))
249
-
250
- validateJsonApiStructure(afterPut)
251
- assertResourceAttributes(afterPut.data, {
252
- name: 'Updated Project',
253
- description: 'Updated Description',
254
- workspace_id: 'workspace-a',
255
- user_id: 101
256
- })
257
-
258
- await assert.rejects(
259
- async () => {
260
- await api.resources.projects.patch({
261
- id: project.data.id,
262
- inputRecord: {
263
- data: {
264
- type: 'projects',
265
- id: project.data.id,
266
- attributes: {
267
- workspace_id: 'workspace-b'
268
- }
269
- }
270
- },
271
- simplified: false
272
- }, scopedContext('workspace-a', 101))
273
- },
274
- (error) => error.code === 'REST_API_VALIDATION',
275
- 'PATCH should reject inconsistent scope-field updates'
276
- )
277
- })
278
-
279
- it('uses scoped lookups when validating relationships', async () => {
280
- const projectA = await api.resources.projects.post({
281
- inputRecord: createJsonApiDocument('projects', {
282
- name: 'Project A'
283
- }),
284
- simplified: false
285
- }, scopedContext('workspace-a', 101))
286
-
287
- const projectB = await api.resources.projects.post({
288
- inputRecord: createJsonApiDocument('projects', {
289
- name: 'Project B'
290
- }),
291
- simplified: false
292
- }, scopedContext('workspace-b', 101))
293
-
294
- const task = await api.resources.tasks.post({
295
- inputRecord: createJsonApiDocument('tasks',
296
- {
297
- title: 'Task A'
298
- },
299
- {
300
- project: createRelationship(resourceIdentifier('projects', projectA.data.id))
301
- }
302
- ),
303
- simplified: false
304
- }, scopedContext('workspace-a', 101))
305
-
306
- validateJsonApiStructure(task)
307
- assertResourceAttributes(task.data, {
308
- title: 'Task A',
309
- workspace_id: 'workspace-a',
310
- user_id: 101
311
- })
312
-
313
- await assert.rejects(
314
- async () => {
315
- await api.resources.tasks.post({
316
- inputRecord: createJsonApiDocument('tasks',
317
- {
318
- title: 'Cross-scope Task'
319
- },
320
- {
321
- project: createRelationship(resourceIdentifier('projects', projectB.data.id))
322
- }
323
- ),
324
- simplified: false
325
- }, scopedContext('workspace-a', 101))
326
- },
327
- (error) => error.code === 'REST_API_RESOURCE' || error.code === 'REST_API_VALIDATION',
328
- 'Relationship validation should respect scoped datasets'
329
- )
330
- })
331
-
332
- it('scopes included resources through the same autofilter path as top-level queries', async () => {
333
- const project = await api.resources.projects.post({
334
- inputRecord: createJsonApiDocument('projects', {
335
- name: 'Scoped include project'
336
- }),
337
- simplified: false
338
- }, scopedContext('workspace-a', 101))
339
-
340
- const visibleTask = await api.resources.tasks.post({
341
- inputRecord: createJsonApiDocument('tasks',
342
- {
343
- title: 'Visible task'
344
- },
345
- {
346
- project: createRelationship(resourceIdentifier('projects', project.data.id))
347
- }
348
- ),
349
- simplified: false
350
- }, scopedContext('workspace-a', 101))
351
-
352
- const hiddenTask = await api.resources.tasks.post({
353
- inputRecord: createJsonApiDocument('tasks',
354
- {
355
- title: 'Out-of-scope task'
356
- },
357
- {
358
- project: createRelationship(resourceIdentifier('projects', project.data.id))
359
- }
360
- ),
361
- simplified: false
362
- }, scopedContext('workspace-a', 101))
363
-
364
- await moveTaskOutOfScope({
365
- taskId: hiddenTask.data.id,
366
- workspaceId: 'workspace-b',
367
- userId: 202
368
- })
369
-
370
- const result = await api.resources.projects.get({
371
- id: project.data.id,
372
- queryParams: {
373
- include: ['tasks']
374
- },
375
- simplified: false
376
- }, scopedContext('workspace-a', 101))
377
-
378
- validateJsonApiStructure(result)
379
- assert.deepEqual(result.data.relationships.tasks.data, [
380
- resourceIdentifier('tasks', visibleTask.data.id)
381
- ])
382
- assert.equal(result.included?.length, 1)
383
- assert.equal(result.included?.[0]?.id, visibleTask.data.id)
384
- assert.equal(result.included?.[0]?.attributes.title, 'Visible task')
385
- })
386
-
387
- it('treats null belongsTo autofilter values as valid null linkage on writes', async () => {
388
- const implicitNull = await api.resources.optional_tasks.post({
389
- inputRecord: createJsonApiDocument('optional_tasks', {
390
- title: 'Implicit null project'
391
- }),
392
- simplified: false
393
- }, scopedContext('workspace-a', 101))
394
-
395
- const explicitNull = await api.resources.optional_tasks.post({
396
- inputRecord: createJsonApiDocument(
397
- 'optional_tasks',
398
- {
399
- title: 'Explicit null project'
400
- },
401
- {
402
- project: { data: null }
403
- }
404
- ),
405
- simplified: false
406
- }, scopedContext('workspace-a', 101))
407
-
408
- const implicitFetched = await api.resources.optional_tasks.get({
409
- id: implicitNull.data.id,
410
- simplified: false
411
- }, scopedContext('workspace-a', 101))
412
-
413
- const explicitFetched = await api.resources.optional_tasks.get({
414
- id: explicitNull.data.id,
415
- simplified: false
416
- }, scopedContext('workspace-a', 101))
417
-
418
- validateJsonApiStructure(implicitFetched)
419
- validateJsonApiStructure(explicitFetched)
420
- assert.equal(implicitFetched.data.relationships.project.data, null)
421
- assert.equal(explicitFetched.data.relationships.project.data, null)
422
- })
423
-
424
- it('fails cleanly when a required autofilter value is missing', async () => {
425
- await assert.rejects(
426
- async () => {
427
- await api.resources.projects.query({
428
- simplified: false
429
- })
430
- },
431
- (error) => error.code === 'REST_API_AUTOFILTER_CONTEXT',
432
- 'Scoped resources should require configured scope values'
433
- )
434
-
435
- await assert.rejects(
436
- async () => {
437
- await api.resources.projects.post({
438
- inputRecord: createJsonApiDocument('projects', {
439
- name: 'No Scope'
440
- }),
441
- simplified: false
442
- })
443
- },
444
- (error) => error.code === 'REST_API_AUTOFILTER_CONTEXT',
445
- 'Scoped writes should require configured scope values'
446
- )
447
- })
448
-
449
- it('exposes compiled preset configuration without auth semantics', () => {
450
- assert.deepEqual(api.autofilter.getConfig(), {
451
- presets: ['public', 'workspace', 'user', 'workspace_user'],
452
- resolvers: ['workspace', 'user', 'currentProject']
453
- })
454
-
455
- assert.deepEqual(api.autofilter.getScopeConfig('projects'), {
456
- preset: 'workspace_user',
457
- filters: [
458
- { field: 'workspace_id', resolver: 'workspace', required: true },
459
- { field: 'user_id', resolver: 'user', required: true }
460
- ]
461
- })
462
- })
463
- })