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,489 +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
- validateJsonApiStructure
11
- } from './helpers/test-utils.js'
12
- import { storageMode } from './helpers/storage-mode.js'
13
- import { createRowPolicyApi } from './fixtures/api-configs.js'
14
- import { createStorageAdapterUtilities } from '../plugins/core/lib/querying/storage-adapter-utils.js'
15
-
16
- const knex = knexLib({
17
- client: 'better-sqlite3',
18
- connection: {
19
- filename: ':memory:'
20
- },
21
- useNullAsDefault: true
22
- })
23
-
24
- let api
25
- const policyEvents = []
26
-
27
- const adminContext = (workspaceId = 'workspace-a') => ({
28
- visibility: { all: true },
29
- scopeValues: { workspaceId }
30
- })
31
- const groupContext = (...groups) => ({
32
- visibility: { groups },
33
- scopeValues: { workspaceId: 'workspace-a' }
34
- })
35
-
36
- const postProject = async (name, accessGroup, context = adminContext()) => {
37
- return api.resources.policy_projects.post({
38
- inputRecord: createJsonApiDocument('policy_projects', {
39
- name,
40
- access_group: accessGroup
41
- }),
42
- simplified: false
43
- }, context)
44
- }
45
-
46
- const postTask = async ({ title, accessGroup, projectId, context = adminContext() }) => {
47
- const relationships = projectId === undefined
48
- ? {}
49
- : {
50
- project: createRelationship(resourceIdentifier('policy_projects', projectId))
51
- }
52
-
53
- return api.resources.policy_tasks.post({
54
- inputRecord: createJsonApiDocument('policy_tasks', {
55
- title,
56
- access_group: accessGroup
57
- }, relationships),
58
- simplified: false
59
- }, context)
60
- }
61
-
62
- describe('RowPolicy Plugin', () => {
63
- it('keeps active query translation stable after the hook context is restored', () => {
64
- const queryStorageAdapter = {
65
- translateColumn: (field) => field === 'id' ? 'record_id' : field
66
- }
67
- const ambientStorageAdapter = {
68
- translateColumn: (field) => field === 'id' ? 'wrong_id' : field
69
- }
70
- const context = {
71
- knexQuery: {
72
- scopeName: 'organisationUnits',
73
- tableName: 'organisation_units',
74
- storageAdapter: queryStorageAdapter
75
- },
76
- storageAdapter: ambientStorageAdapter
77
- }
78
- const utilities = createStorageAdapterUtilities({ context })
79
-
80
- delete context.knexQuery
81
- delete context.storageAdapter
82
-
83
- assert.equal(
84
- utilities.translateColumn('organisationUnits', 'id'),
85
- 'organisation_units.record_id'
86
- )
87
- assert.equal(
88
- utilities.translateColumn('organisationUnits', 'id', 'visible_units'),
89
- 'visible_units.record_id'
90
- )
91
- assert.equal(utilities.defaultAliasForScope('users'), 'users')
92
- assert.equal(Object.hasOwn(context, 'knexQuery'), false)
93
- assert.equal(Object.hasOwn(context, 'storageAdapter'), false)
94
- })
95
-
96
- before(async () => {
97
- api = await createRowPolicyApi(knex, {
98
- onPolicy: (event) => policyEvents.push(event)
99
- })
100
- })
101
-
102
- after(async () => {
103
- await knex.destroy()
104
- })
105
-
106
- beforeEach(async () => {
107
- policyEvents.length = 0
108
- await cleanTables(knex, [
109
- 'row_policy_project_tasks',
110
- 'row_policy_projects',
111
- 'row_policy_tasks',
112
- 'row_policy_broken'
113
- ])
114
- })
115
-
116
- it('applies deferred grouped visibility before offset pagination and counts the same dataset', async () => {
117
- await postProject('Allowed 1', 'group-a')
118
- await postProject('Hidden 1', 'group-b')
119
- await postProject('Allowed 2', 'group-a')
120
- await postProject('Hidden 2', 'group-b')
121
- await postProject('Allowed 3', 'group-a')
122
-
123
- policyEvents.length = 0
124
- const firstPage = await api.resources.policy_projects.query({
125
- queryParams: {
126
- sort: ['id'],
127
- page: { number: 1, size: 2 }
128
- },
129
- simplified: false
130
- }, groupContext('group-a'))
131
-
132
- validateJsonApiStructure(firstPage, true)
133
- assert.deepEqual(
134
- firstPage.data.map((record) => record.attributes.name),
135
- ['Allowed 1', 'Allowed 2']
136
- )
137
- assert.equal(firstPage.meta.pagination.total, 3)
138
- assert.equal(firstPage.meta.pagination.pageCount, 2)
139
- assert(policyEvents.some((event) => event.queryPurpose === 'collection'))
140
-
141
- const secondPage = await api.resources.policy_projects.query({
142
- queryParams: {
143
- sort: ['id'],
144
- page: { number: 2, size: 2 }
145
- },
146
- simplified: false
147
- }, groupContext('group-a'))
148
-
149
- assert.deepEqual(
150
- secondPage.data.map((record) => record.attributes.name),
151
- ['Allowed 3']
152
- )
153
- })
154
-
155
- it('combines row policy visibility with the resource autofilter', async () => {
156
- await postProject('Visible', 'group-a')
157
- await postProject('Wrong group', 'group-b')
158
- await postProject('Wrong workspace', 'group-a', adminContext('workspace-b'))
159
-
160
- const result = await api.resources.policy_projects.query({
161
- queryParams: { sort: ['id'] },
162
- simplified: false
163
- }, groupContext('group-a'))
164
-
165
- assert.deepEqual(
166
- result.data.map((record) => record.attributes.name),
167
- ['Visible']
168
- )
169
- })
170
-
171
- it('keeps cursor pagination inside the visible dataset', async () => {
172
- await postProject('Allowed 1', 'group-a')
173
- await postProject('Hidden 1', 'group-b')
174
- await postProject('Allowed 2', 'group-a')
175
- await postProject('Hidden 2', 'group-b')
176
- await postProject('Allowed 3', 'group-a')
177
-
178
- const firstPage = await api.resources.policy_projects.query({
179
- queryParams: {
180
- sort: ['name'],
181
- page: { size: 2 }
182
- },
183
- simplified: false
184
- }, groupContext('group-a'))
185
-
186
- assert.deepEqual(
187
- firstPage.data.map((record) => record.attributes.name),
188
- ['Allowed 1', 'Allowed 2']
189
- )
190
- assert.equal(firstPage.meta.pagination.hasMore, true)
191
- assert(firstPage.meta.pagination.cursor.next)
192
-
193
- const secondPage = await api.resources.policy_projects.query({
194
- queryParams: {
195
- sort: ['name'],
196
- page: {
197
- size: 2,
198
- after: firstPage.meta.pagination.cursor.next
199
- }
200
- },
201
- simplified: false
202
- }, groupContext('group-a'))
203
-
204
- assert.deepEqual(
205
- secondPage.data.map((record) => record.attributes.name),
206
- ['Allowed 3']
207
- )
208
- assert.equal(secondPage.meta.pagination.hasMore, false)
209
- })
210
-
211
- if (storageMode.isAnyApi()) {
212
- it('applies the policy to the standalone AnyAPI count helper', async () => {
213
- await postProject('Allowed 1', 'group-a')
214
- await postProject('Hidden 1', 'group-b')
215
- await postProject('Allowed 2', 'group-a')
216
-
217
- policyEvents.length = 0
218
- const total = await api.helpers.dataQueryCount({
219
- scopeName: 'policy_projects',
220
- context: {
221
- db: knex,
222
- queryParams: {},
223
- ...groupContext('group-a')
224
- }
225
- })
226
-
227
- assert.equal(total, 2)
228
- assert(policyEvents.some((event) => event.queryPurpose === 'count'))
229
- })
230
- }
231
-
232
- it('uses the policy for single-record and write preflight lookups', async () => {
233
- const allowed = await postProject('Allowed', 'group-a')
234
- const hidden = await postProject('Hidden', 'group-b')
235
-
236
- const visibleRecord = await api.resources.policy_projects.get({
237
- id: allowed.data.id,
238
- simplified: false
239
- }, groupContext('group-a'))
240
- assert.equal(visibleRecord.data.attributes.name, 'Allowed')
241
-
242
- await assert.rejects(
243
- api.resources.policy_projects.get({
244
- id: hidden.data.id,
245
- simplified: false
246
- }, groupContext('group-a')),
247
- (error) => error.code === 'REST_API_RESOURCE'
248
- )
249
-
250
- await assert.rejects(
251
- api.resources.policy_projects.patch({
252
- id: hidden.data.id,
253
- inputRecord: {
254
- data: {
255
- type: 'policy_projects',
256
- id: hidden.data.id,
257
- attributes: { name: 'Changed' }
258
- }
259
- },
260
- simplified: false
261
- }, groupContext('group-a')),
262
- (error) => error.code === 'REST_API_RESOURCE'
263
- )
264
-
265
- await assert.rejects(
266
- api.resources.policy_projects.delete({
267
- id: hidden.data.id,
268
- simplified: false
269
- }, groupContext('group-a')),
270
- (error) => error.code === 'REST_API_RESOURCE'
271
- )
272
-
273
- const stillPresent = await api.resources.policy_projects.get({
274
- id: hidden.data.id,
275
- simplified: false
276
- }, adminContext())
277
- assert.equal(stillPresent.data.attributes.name, 'Hidden')
278
- })
279
-
280
- it('filters included children and relationship identifiers', async () => {
281
- const project = await postProject('Visible project', 'group-a')
282
- const visibleTask = await postTask({
283
- title: 'Visible task',
284
- accessGroup: 'group-a',
285
- projectId: project.data.id
286
- })
287
- await postTask({
288
- title: 'Hidden task',
289
- accessGroup: 'group-b',
290
- projectId: project.data.id
291
- })
292
-
293
- policyEvents.length = 0
294
- const result = await api.resources.policy_projects.get({
295
- id: project.data.id,
296
- queryParams: { include: ['tasks'] },
297
- simplified: false
298
- }, groupContext('group-a'))
299
-
300
- assert.deepEqual(result.data.relationships.tasks.data, [
301
- resourceIdentifier('policy_tasks', visibleTask.data.id)
302
- ])
303
- assert.deepEqual(
304
- result.included.map((record) => record.attributes.title),
305
- ['Visible task']
306
- )
307
- assert(policyEvents.some((event) => event.queryPurpose === 'relationship-identifiers'))
308
- assert(policyEvents.some((event) => event.queryPurpose === 'include'))
309
-
310
- policyEvents.length = 0
311
- const relationship = await api.resources.policy_projects.getRelationship({
312
- id: project.data.id,
313
- relationshipName: 'tasks',
314
- simplified: false
315
- }, groupContext('group-a'))
316
-
317
- assert.deepEqual(relationship.data, [
318
- resourceIdentifier('policy_tasks', visibleTask.data.id)
319
- ])
320
- assert(policyEvents.some((event) => event.queryPurpose === 'single'))
321
- })
322
-
323
- it('filters a belongs-to include through the target resource policy', async () => {
324
- const hiddenProject = await postProject('Hidden project', 'group-b')
325
- await postTask({
326
- title: 'Visible task',
327
- accessGroup: 'group-a',
328
- projectId: hiddenProject.data.id
329
- })
330
-
331
- const result = await api.resources.policy_tasks.query({
332
- queryParams: { include: ['project'], sort: ['id'] },
333
- simplified: false
334
- }, groupContext('group-a'))
335
-
336
- assert.equal(result.data.length, 1)
337
- assert.equal(result.included?.length || 0, 0)
338
- })
339
-
340
- it('uses target row policies during relationship validation', async () => {
341
- const hiddenProject = await postProject('Hidden project', 'group-b')
342
-
343
- await assert.rejects(
344
- postTask({
345
- title: 'Cross-policy task',
346
- accessGroup: 'group-a',
347
- projectId: hiddenProject.data.id,
348
- context: groupContext('group-a')
349
- }),
350
- (error) => error.code === 'REST_API_RESOURCE' || error.code === 'REST_API_VALIDATION'
351
- )
352
-
353
- assert(policyEvents.some((event) => event.queryPurpose === 'relationship-validation'))
354
- })
355
-
356
- it('uses target row policies when adding many-to-many relationships', async () => {
357
- const project = await postProject('Visible project', 'group-a')
358
- const visibleTask = await postTask({ title: 'Visible task', accessGroup: 'group-a' })
359
- const hiddenTask = await postTask({ title: 'Hidden task', accessGroup: 'group-b' })
360
-
361
- await api.resources.policy_projects.postRelationship({
362
- id: project.data.id,
363
- relationshipName: 'shared_tasks',
364
- relationshipData: [resourceIdentifier('policy_tasks', visibleTask.data.id)],
365
- simplified: false
366
- }, groupContext('group-a'))
367
-
368
- await assert.rejects(
369
- api.resources.policy_projects.postRelationship({
370
- id: project.data.id,
371
- relationshipName: 'shared_tasks',
372
- relationshipData: [resourceIdentifier('policy_tasks', hiddenTask.data.id)],
373
- simplified: false
374
- }, groupContext('group-a')),
375
- (error) => error.code === 'REST_API_RESOURCE'
376
- )
377
-
378
- assert.equal(await countRecords(knex, 'row_policy_project_tasks'), 1)
379
- })
380
-
381
- it('does not expose relationship routes for a hidden parent', async () => {
382
- const hiddenProject = await postProject('Hidden project', 'group-b')
383
-
384
- await assert.rejects(
385
- api.resources.policy_projects.getRelated({
386
- id: hiddenProject.data.id,
387
- relationshipName: 'tasks',
388
- queryParams: {},
389
- simplified: false
390
- }, groupContext('group-a')),
391
- (error) => error.code === 'REST_API_RESOURCE'
392
- )
393
-
394
- await assert.rejects(
395
- api.resources.policy_projects.getRelationship({
396
- id: hiddenProject.data.id,
397
- relationshipName: 'tasks',
398
- simplified: false
399
- }, groupContext('group-a')),
400
- (error) => error.code === 'REST_API_RESOURCE'
401
- )
402
-
403
- await assert.rejects(
404
- api.resources.policy_projects.patchRelationship({
405
- id: hiddenProject.data.id,
406
- relationshipName: 'tasks',
407
- relationshipData: [],
408
- simplified: false
409
- }, groupContext('group-a')),
410
- (error) => error.code === 'REST_API_RESOURCE'
411
- )
412
-
413
- assert(policyEvents.some((event) => event.queryPurpose === 'relationship-parent'))
414
- })
415
-
416
- it('filters related children before pagination and keeps target filters off the parent lookup', async () => {
417
- const project = await postProject('Visible project', 'group-a')
418
- await postTask({ title: 'Allowed 1', accessGroup: 'group-a', projectId: project.data.id })
419
- await postTask({ title: 'Hidden 1', accessGroup: 'group-b', projectId: project.data.id })
420
- await postTask({ title: 'Allowed 2', accessGroup: 'group-a', projectId: project.data.id })
421
- await postTask({ title: 'Hidden 2', accessGroup: 'group-b', projectId: project.data.id })
422
- await postTask({ title: 'Allowed 3', accessGroup: 'group-a', projectId: project.data.id })
423
-
424
- const firstPage = await api.resources.policy_projects.getRelated({
425
- id: project.data.id,
426
- relationshipName: 'tasks',
427
- queryParams: {
428
- sort: ['id'],
429
- page: { number: 1, size: 2 }
430
- },
431
- simplified: false
432
- }, groupContext('group-a'))
433
-
434
- assert.deepEqual(
435
- firstPage.data.map((record) => record.attributes.title),
436
- ['Allowed 1', 'Allowed 2']
437
- )
438
- assert.equal(firstPage.meta.pagination.total, 3)
439
-
440
- const filtered = await api.resources.policy_projects.getRelated({
441
- id: project.data.id,
442
- relationshipName: 'tasks',
443
- queryParams: {
444
- filters: { title: 'Allowed 3' },
445
- page: { number: 1, size: 2 }
446
- },
447
- simplified: false
448
- }, groupContext('group-a'))
449
-
450
- assert.deepEqual(
451
- filtered.data.map((record) => record.attributes.title),
452
- ['Allowed 3']
453
- )
454
- assert.equal(filtered.meta.pagination.total, 1)
455
- })
456
-
457
- it('denies all rows when a policy explicitly returns false', async () => {
458
- await postProject('Hidden without context', 'group-a')
459
-
460
- const result = await api.resources.policy_projects.query({
461
- queryParams: { page: { number: 1, size: 10 } },
462
- simplified: false
463
- }, { scopeValues: { workspaceId: 'workspace-a' } })
464
-
465
- assert.deepEqual(result.data, [])
466
- assert.equal(result.meta.pagination.total, 0)
467
- })
468
-
469
- it('fails closed when a policy does not return an explicit decision', async () => {
470
- await assert.rejects(
471
- api.resources.policy_broken.query({ simplified: false }),
472
- (error) => error.code === 'REST_API_ROW_POLICY_CONTRACT'
473
- )
474
- })
475
-
476
- it('exposes compiled policy configuration for inspection', () => {
477
- assert.deepEqual(api.rowPolicies.getConfig(), {
478
- policies: ['groupVisibility']
479
- })
480
- assert.deepEqual(api.rowPolicies.getScopeConfig('policy_projects'), {
481
- policy: 'groupVisibility',
482
- source: 'registry'
483
- })
484
- assert.deepEqual(api.rowPolicies.getScopeConfig('policy_broken'), {
485
- policy: '<inline>',
486
- source: 'inline'
487
- })
488
- })
489
- })
@@ -1,38 +0,0 @@
1
- import { describe, it } from 'node:test'
2
- import assert from 'node:assert/strict'
3
-
4
- import { S3Storage } from '../index.js'
5
-
6
- function createFile (filename = 'photo.png') {
7
- return {
8
- filename,
9
- mimetype: 'image/png',
10
- data: Buffer.from('file contents')
11
- }
12
- }
13
-
14
- describe('S3Storage', () => {
15
- it('requires a bucket name', () => {
16
- assert.throws(() => new S3Storage(), /S3Storage requires a bucket name/)
17
- })
18
-
19
- it('generates S3-style mock URLs and accepts deleting returned URLs', async () => {
20
- const storage = new S3Storage({
21
- bucket: 'my-uploads',
22
- region: 'ap-southeast-2',
23
- prefix: 'uploads/'
24
- })
25
-
26
- const url = await storage.upload(createFile())
27
-
28
- assert.match(url, /^https:\/\/my-uploads\.s3\.ap-southeast-2\.amazonaws\.com\/uploads\/[a-f0-9]{32}\.png$/)
29
- await assert.doesNotReject(storage.delete(url))
30
- })
31
-
32
- it('fails clearly when real S3 mode is requested', () => {
33
- assert.throws(
34
- () => new S3Storage({ bucket: 'my-uploads', mockMode: false }),
35
- /S3Storage real S3 mode is not implemented/
36
- )
37
- })
38
- })