json-rest-api 1.0.28 → 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 (289) 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 +746 -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} +90 -31
  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 -174
  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 +67 -330
  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 +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -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 +238 -187
  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 +111 -287
  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 +109 -44
  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 +142 -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 -2509
  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 -1069
  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 -385
  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 -145
  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 +38 -69
  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 +39 -82
  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 -1238
  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 -150
  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 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  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-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. 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
- })