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,560 +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 { cleanTables } from './helpers/test-utils.js'
5
- import { createPositioningApi } from './fixtures/api-configs.js'
6
- import { PositioningPlugin } from '../plugins/core/rest-api-positioning-plugin.js'
7
-
8
- // Create Knex instance for tests - always use SQLite in-memory
9
- const knex = knexLib({
10
- client: 'better-sqlite3',
11
- connection: {
12
- filename: ':memory:'
13
- },
14
- useNullAsDefault: true
15
- })
16
-
17
- // API instance that persists across tests
18
- let api
19
-
20
- describe('Positioning Plugin Tests', { timeout: 30000 }, () => {
21
- // IMPORTANT: before() runs ONCE for the entire test suite
22
- before(async () => {
23
- // Initialize API once
24
- api = await createPositioningApi(knex)
25
-
26
- // Install positioning plugin with default configuration
27
- await api.use(PositioningPlugin, {
28
- field: 'position',
29
- filters: ['category'], // Tasks will be grouped by category
30
- defaultPosition: 'last',
31
- autoIndex: true
32
- })
33
- })
34
-
35
- // IMPORTANT: after() cleans up resources
36
- after(async () => {
37
- // Always destroy knex connection to allow tests to exit
38
- await knex.destroy()
39
- })
40
-
41
- // IMPORTANT: beforeEach() cleans data but does NOT recreate API
42
- beforeEach(async () => {
43
- // Clean all tables - list ALL tables your tests use
44
- await cleanTables(knex, [
45
- 'positioning_categories',
46
- 'positioning_tasks',
47
- 'positioning_projects',
48
- 'positioning_items'
49
- ])
50
- })
51
-
52
- describe('Basic Positioning', () => {
53
- it('should add position field to first item automatically', async () => {
54
- // Create a category first
55
- const category = await api.resources.categories.post({
56
- name: 'Work Tasks'
57
- })
58
-
59
- // Create first task
60
- const result = await api.resources.tasks.post({
61
- title: 'First Task',
62
- category: category.id
63
- })
64
-
65
- assert.equal(result.title, 'First Task')
66
- // Should have position assigned
67
- assert(result.position, 'Position should be assigned')
68
- assert.equal(typeof result.position, 'string')
69
- })
70
-
71
- it('should position items at the end by default', async () => {
72
- // Create a category
73
- const category = await api.resources.categories.post({ name: 'Test Category' })
74
-
75
- // Create multiple tasks
76
- const task1 = await api.resources.tasks.post({
77
- title: 'Task 1',
78
- category: category.id
79
- })
80
-
81
- const task2 = await api.resources.tasks.post({
82
- title: 'Task 2',
83
- category: category.id
84
- })
85
-
86
- const task3 = await api.resources.tasks.post({
87
- title: 'Task 3',
88
- category: category.id
89
- })
90
-
91
- // Verify positions are in order
92
- const pos1 = task1.position
93
- const pos2 = task2.position
94
- const pos3 = task3.position
95
-
96
- assert(pos1 < pos2, 'Task 1 should be before Task 2')
97
- assert(pos2 < pos3, 'Task 2 should be before Task 3')
98
- })
99
-
100
- it('should maintain separate position sequences per filter group', async () => {
101
- // Create two categories
102
- const category1 = await api.resources.categories.post({ name: 'Category 1' })
103
- const category2 = await api.resources.categories.post({ name: 'Category 2' })
104
-
105
- // Create tasks in different categories
106
- const task1Cat1 = await api.resources.tasks.post({
107
- title: 'Cat1 Task 1',
108
- category: category1.id
109
- })
110
-
111
- const task1Cat2 = await api.resources.tasks.post({
112
- title: 'Cat2 Task 1',
113
- category: category2.id
114
- })
115
-
116
- const task2Cat1 = await api.resources.tasks.post({
117
- title: 'Cat1 Task 2',
118
- category: category1.id
119
- })
120
-
121
- // Positions should be independent per category
122
- assert(task1Cat1.position)
123
- assert(task1Cat2.position)
124
- assert(task2Cat1.position)
125
-
126
- // Task 2 in category 1 should be after task 1 in category 1
127
- assert(task1Cat1.position < task2Cat1.position)
128
-
129
- // But positions in different categories can be similar
130
- // (they're independent sequences)
131
- })
132
- })
133
-
134
- describe('Positioning with beforeId', () => {
135
- it('should position item before specified id', async () => {
136
- // Create category
137
- const category = await api.resources.categories.post({ name: 'Test Category' })
138
-
139
- // Create initial tasks
140
- const task1 = await api.resources.tasks.post({
141
- title: 'Task 1',
142
- category: category.id
143
- })
144
-
145
- const task2 = await api.resources.tasks.post({
146
- title: 'Task 2',
147
- category: category.id
148
- })
149
-
150
- // Create task that should go between 1 and 2
151
- const taskMiddle = await api.resources.tasks.post({
152
- title: 'Middle Task',
153
- category: category.id,
154
- beforeId: task2.id
155
- })
156
-
157
- // Verify positioning
158
- const pos1 = task1.position
159
- const posMiddle = taskMiddle.position
160
- const pos2 = task2.position
161
-
162
- assert(pos1 < posMiddle, 'Task 1 should be before Middle Task')
163
- assert(posMiddle < pos2, 'Middle Task should be before Task 2')
164
- })
165
-
166
- it('should position at beginning when beforeId is first item', async () => {
167
- // Create category
168
- const category = await api.resources.categories.post({ name: 'Test Category' })
169
-
170
- // Create initial tasks
171
- const task1 = await api.resources.tasks.post({
172
- title: 'Task 1',
173
- category: category.id
174
- })
175
-
176
- const task2 = await api.resources.tasks.post({
177
- title: 'Task 2',
178
- category: category.id
179
- })
180
-
181
- // Create task at beginning
182
- const taskFirst = await api.resources.tasks.post({
183
- title: 'First Task',
184
- category: category.id,
185
- beforeId: task1.id
186
- })
187
-
188
- // Verify it's first
189
- const posFirst = taskFirst.position
190
- const pos1 = task1.position
191
- const pos2 = task2.position
192
-
193
- assert(posFirst < pos1, 'First Task should be before Task 1')
194
- assert(pos1 < pos2, 'Task 1 should be before Task 2')
195
- })
196
- })
197
-
198
- describe('Position Updates', () => {
199
- it('should reposition item when updated with beforeId', async () => {
200
- // Create category
201
- const category = await api.resources.categories.post({ name: 'Test Category' })
202
-
203
- // Create three tasks
204
- const task1 = await api.resources.tasks.post({
205
- title: 'Task 1',
206
- category: category.id
207
- })
208
-
209
- const task2 = await api.resources.tasks.post({
210
- title: 'Task 2',
211
- category: category.id
212
- })
213
-
214
- const task3 = await api.resources.tasks.post({
215
- title: 'Task 3',
216
- category: category.id
217
- })
218
-
219
- // Move task 3 before task 2
220
- const updated = await api.resources.tasks.patch({
221
- id: task3.id,
222
- beforeId: task2.id
223
- })
224
-
225
- // Get fresh data
226
- const { data: allTasks } = await api.resources.tasks.query({
227
- queryParams: {
228
- filters: { category: category.id },
229
- sort: ['position']
230
- }
231
- })
232
-
233
- // Verify order is now: Task 1, Task 3, Task 2
234
- assert.equal(allTasks[0].title, 'Task 1')
235
- assert.equal(allTasks[1].title, 'Task 3')
236
- assert.equal(allTasks[2].title, 'Task 2')
237
- })
238
-
239
- it('should maintain position when updating without beforeId', async () => {
240
- // Create category
241
- const category = await api.resources.categories.post({ name: 'Test Category' })
242
-
243
- // Create task
244
- const task = await api.resources.tasks.post({
245
- title: 'Original Title',
246
- category: category.id
247
- })
248
-
249
- const originalPosition = task.position
250
-
251
- // Update title without beforeId
252
- const updated = await api.resources.tasks.patch({
253
- id: task.id,
254
- title: 'Updated Title'
255
- })
256
-
257
- // Position should remain the same
258
- assert.equal(updated.position, originalPosition)
259
- assert.equal(updated.title, 'Updated Title')
260
- })
261
- })
262
-
263
- describe('Manual Position Override', () => {
264
- it('should ignore manual position setting', async () => {
265
- // Create category
266
- const category = await api.resources.categories.post({ name: 'Test Category' })
267
-
268
- // Create task with manual position attempt
269
- const task = await api.resources.tasks.post({
270
- title: 'Manual Position Task',
271
- category: category.id,
272
- position: 'zzz' // Manual position should be ignored
273
- })
274
-
275
- // Position should be automatically assigned, not 'zzz'
276
- assert.notEqual(task.position, 'zzz')
277
- assert(task.position) // Should have a position
278
- assert.equal(task.position, 'a0') // First item gets 'a0'
279
- })
280
-
281
- it('should ignore manual position and use beforeId', async () => {
282
- // Create category
283
- const category = await api.resources.categories.post({ name: 'Test Category' })
284
-
285
- // Create initial task
286
- const task1 = await api.resources.tasks.post({
287
- title: 'Task 1',
288
- category: category.id
289
- })
290
-
291
- // Create task with both manual position and beforeId
292
- const task2 = await api.resources.tasks.post({
293
- title: 'Task 2',
294
- category: category.id,
295
- position: 'zzz', // Should be ignored
296
- beforeId: task1.id // Should be used
297
- })
298
-
299
- // Manual position should be ignored, beforeId should work
300
- assert.notEqual(task2.position, 'zzz')
301
- assert(task2.position < task1.position) // Should be positioned before task1
302
- })
303
- })
304
-
305
- describe('Position Configuration', () => {
306
- it('should handle different field name configuration', async () => {
307
- // Create a new API with different position field name
308
- const customApi = await createPositioningApi(knex, {
309
- apiName: 'positioning-custom-field-test',
310
- tablePrefix: 'pos_custom'
311
- })
312
-
313
- await customApi.use(PositioningPlugin, {
314
- field: 'sort_order', // Different field name
315
- filters: ['project'],
316
- defaultPosition: 'last'
317
- })
318
-
319
- // Create project
320
- const project = await customApi.resources.projects.post({ name: 'Test Project' })
321
-
322
- // Create item
323
- const item = await customApi.resources.items.post({
324
- name: 'Test Item',
325
- project: project.id
326
- })
327
-
328
- // Should have sort_order field, not position
329
- assert(item.sort_order, 'Should have sort_order field')
330
- assert(!item.position, 'Should not have position field')
331
- })
332
-
333
- it('should handle multiple filter fields', async () => {
334
- // Create a new API with multiple filters
335
- const multiFilterApi = await createPositioningApi(knex, {
336
- apiName: 'positioning-multi-filter-test',
337
- tablePrefix: 'pos_multi'
338
- })
339
-
340
- await multiFilterApi.use(PositioningPlugin, {
341
- field: 'position',
342
- filters: ['project', 'status'], // Multiple filters
343
- defaultPosition: 'last'
344
- })
345
-
346
- // Create project
347
- const project = await multiFilterApi.resources.projects.post({ name: 'Multi Filter Project' })
348
-
349
- // Create items with different statuses
350
- const item1Active = await multiFilterApi.resources.items.post({
351
- name: 'Active Item 1',
352
- project: project.id,
353
- status: 'active'
354
- })
355
-
356
- const item2Active = await multiFilterApi.resources.items.post({
357
- name: 'Active Item 2',
358
- project: project.id,
359
- status: 'active'
360
- })
361
-
362
- const item1Archived = await multiFilterApi.resources.items.post({
363
- name: 'Archived Item 1',
364
- project: project.id,
365
- status: 'archived'
366
- })
367
-
368
- // Active items should be in sequence
369
- assert(item1Active.position < item2Active.position)
370
-
371
- // Archived item should have its own sequence
372
- // (its position might be similar to active item 1, but they're in different groups)
373
- })
374
-
375
- it('should handle defaultPosition first configuration', async () => {
376
- // Create API with defaultPosition: 'first'
377
- const firstApi = await createPositioningApi(knex, {
378
- apiName: 'positioning-first-test',
379
- tablePrefix: 'pos_first'
380
- })
381
-
382
- await firstApi.use(PositioningPlugin, {
383
- field: 'position',
384
- filters: ['category'],
385
- defaultPosition: 'first' // Items go to beginning by default
386
- })
387
-
388
- // Create category
389
- const category = await firstApi.resources.categories.post({ name: 'First Default Category' })
390
-
391
- // Create tasks - they should be added at the beginning
392
- const task1 = await firstApi.resources.tasks.post({
393
- title: 'Task 1',
394
- category: category.id
395
- })
396
-
397
- const task2 = await firstApi.resources.tasks.post({
398
- title: 'Task 2',
399
- category: category.id
400
- })
401
-
402
- const task3 = await firstApi.resources.tasks.post({
403
- title: 'Task 3',
404
- category: category.id
405
- })
406
-
407
- // With defaultPosition: 'first', the order should be reversed
408
- // Task 3 should be first, then Task 2, then Task 1
409
- assert(task3.position < task2.position)
410
- assert(task2.position < task1.position)
411
- })
412
- })
413
-
414
- describe('Error Handling', () => {
415
- it('should handle invalid beforeId gracefully', async () => {
416
- // Create category
417
- const category = await api.resources.categories.post({ name: 'Test Category' })
418
-
419
- // Create task with non-existent beforeId
420
- const task = await api.resources.tasks.post({
421
- title: 'Task with Invalid BeforeId',
422
- category: category.id,
423
- beforeId: '99999' // Non-existent ID
424
- })
425
-
426
- // Should still create the task (positioned at end)
427
- assert(task.position)
428
- assert.equal(task.title, 'Task with Invalid BeforeId')
429
- })
430
-
431
- it('should handle null filter values', async () => {
432
- // Create task without category (null filter)
433
- const task = await api.resources.tasks.post({
434
- title: 'Task without Category',
435
- category: null
436
- })
437
-
438
- // Should still get position
439
- assert(task.position)
440
-
441
- // Create another task without category
442
- const task2 = await api.resources.tasks.post({
443
- title: 'Another Task without Category',
444
- category: null
445
- })
446
-
447
- // They should be in sequence
448
- assert(task.position < task2.position)
449
- })
450
- })
451
-
452
- describe('Excluded Resources', () => {
453
- it('should not add positioning to excluded resources', async () => {
454
- // Create API with excluded resources
455
- const excludeApi = await createPositioningApi(knex, {
456
- apiName: 'positioning-exclude-test',
457
- tablePrefix: 'pos_exclude'
458
- })
459
-
460
- await excludeApi.use(PositioningPlugin, {
461
- field: 'position',
462
- filters: [],
463
- excludeResources: ['categories'] // Exclude categories from positioning
464
- })
465
-
466
- // Create category - should not have position
467
- const category = await excludeApi.resources.categories.post({ name: 'Excluded Category' })
468
-
469
- assert(!category.position, 'Category should not have position')
470
-
471
- // Create task - should have position
472
- const task = await excludeApi.resources.tasks.post({
473
- title: 'Task with Position',
474
- category: category.id
475
- })
476
-
477
- assert(task.position, 'Task should have position')
478
- })
479
- })
480
-
481
- describe('Simplified API Mode', () => {
482
- it('should handle positioning in simplified mode', async () => {
483
- // We're already using simplified mode throughout these tests!
484
- // This test just verifies it works as expected
485
-
486
- // Create category
487
- const category = await api.resources.categories.post({ name: 'Simplified Category' })
488
-
489
- // Create tasks in simplified mode
490
- const task1 = await api.resources.tasks.post({
491
- title: 'Simplified Task 1',
492
- category: category.id
493
- })
494
-
495
- const task2 = await api.resources.tasks.post({
496
- title: 'Simplified Task 2',
497
- category: category.id,
498
- beforeId: task1.id
499
- })
500
-
501
- // Verify positioning
502
- assert(task1.position, 'Task 1 should have position')
503
- assert(task2.position, 'Task 2 should have position')
504
- assert(task2.position < task1.position, 'Task 2 should be before Task 1')
505
- })
506
- })
507
-
508
- describe('Query Ordering', () => {
509
- it('should allow sorting by position field', async () => {
510
- // Create category
511
- const category = await api.resources.categories.post({ name: 'Sort Test Category' })
512
-
513
- // Create tasks in specific order
514
- const taskC = await api.resources.tasks.post({
515
- title: 'C Task',
516
- category: category.id
517
- })
518
-
519
- const taskA = await api.resources.tasks.post({
520
- title: 'A Task',
521
- category: category.id,
522
- beforeId: taskC.id
523
- })
524
-
525
- const taskB = await api.resources.tasks.post({
526
- title: 'B Task',
527
- category: category.id,
528
- beforeId: taskC.id
529
- })
530
-
531
- // Query with position sort
532
- const { data: sorted } = await api.resources.tasks.query({
533
- queryParams: {
534
- filters: { category: category.id },
535
- sort: ['position']
536
- }
537
- })
538
-
539
- // Should be in position order: A, B, C
540
- // Note: When both A and B are positioned before C, they maintain creation order
541
- assert.equal(sorted.length, 3)
542
- assert.equal(sorted[0].title, 'A Task')
543
- assert.equal(sorted[1].title, 'B Task')
544
- assert.equal(sorted[2].title, 'C Task')
545
-
546
- // Query with reverse position sort
547
- const { data: reverseSorted } = await api.resources.tasks.query({
548
- queryParams: {
549
- filters: { category: category.id },
550
- sort: ['-position']
551
- }
552
- })
553
-
554
- // Should be in reverse order: C, B, A
555
- assert.equal(reverseSorted[0].title, 'C Task')
556
- assert.equal(reverseSorted[1].title, 'B Task')
557
- assert.equal(reverseSorted[2].title, 'A Task')
558
- })
559
- })
560
- })
@@ -1,120 +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
- cleanTables,
6
- countRecords,
7
- createJsonApiDocument,
8
- createRelationship,
9
- resourceIdentifier
10
- } from './helpers/test-utils.js'
11
- import { createBasicApi } from './fixtures/api-configs.js'
12
-
13
- const knex = knexLib({
14
- client: 'better-sqlite3',
15
- connection: {
16
- filename: ':memory:'
17
- },
18
- useNullAsDefault: true
19
- })
20
-
21
- let api
22
- let afterCommitFailureMethod = null
23
- const rollbackEvents = []
24
-
25
- describe('Post-commit hook failure semantics', () => {
26
- before(async () => {
27
- api = await createBasicApi(knex, {
28
- apiName: 'post-commit-hook-test',
29
- tablePrefix: 'post_commit'
30
- })
31
-
32
- await api.customize({
33
- hooks: {
34
- afterCommit: {
35
- functionName: 'post-commit-failure-test',
36
- handler: async ({ context }) => {
37
- if (context.method === afterCommitFailureMethod) {
38
- throw new Error(`afterCommit failed for ${context.method}`)
39
- }
40
- }
41
- },
42
- afterRollback: {
43
- functionName: 'post-commit-rollback-tracker',
44
- handler: async ({ context }) => {
45
- rollbackEvents.push(context.method)
46
- }
47
- }
48
- }
49
- })
50
- })
51
-
52
- after(async () => {
53
- await knex.destroy()
54
- })
55
-
56
- beforeEach(async () => {
57
- afterCommitFailureMethod = null
58
- rollbackEvents.length = 0
59
-
60
- await cleanTables(knex, [
61
- 'post_commit_countries',
62
- 'post_commit_publishers',
63
- 'post_commit_authors',
64
- 'post_commit_books',
65
- 'post_commit_book_authors'
66
- ])
67
- })
68
-
69
- it('surfaces afterCommit errors without rolling back committed resource writes', async () => {
70
- afterCommitFailureMethod = 'post'
71
-
72
- await assert.rejects(
73
- api.resources.countries.post({
74
- inputRecord: createJsonApiDocument('countries', {
75
- name: 'Committed Country',
76
- code: 'CC'
77
- }),
78
- simplified: false
79
- }),
80
- /afterCommit failed for post/
81
- )
82
-
83
- assert.equal(await countRecords(knex, 'post_commit_countries'), 1)
84
- assert.deepEqual(rollbackEvents, [])
85
- })
86
-
87
- it('surfaces afterCommit errors without rolling back committed relationship writes', async () => {
88
- const country = await api.resources.countries.post({
89
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' }),
90
- simplified: false
91
- })
92
- const book = await api.resources.books.post({
93
- inputRecord: createJsonApiDocument(
94
- 'books',
95
- { title: 'Committed Book' },
96
- { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
97
- ),
98
- simplified: false
99
- })
100
- const author = await api.resources.authors.post({
101
- inputRecord: createJsonApiDocument('authors', { name: 'Committed Author' }),
102
- simplified: false
103
- })
104
-
105
- rollbackEvents.length = 0
106
- afterCommitFailureMethod = 'postRelationship'
107
-
108
- await assert.rejects(
109
- api.resources.books.postRelationship({
110
- id: book.data.id,
111
- relationshipName: 'authors',
112
- relationshipData: [resourceIdentifier('authors', author.data.id)]
113
- }),
114
- /afterCommit failed for postRelationship/
115
- )
116
-
117
- assert.equal(await countRecords(knex, 'post_commit_book_authors'), 1)
118
- assert.deepEqual(rollbackEvents, [])
119
- })
120
- })