@rebasepro/server 0.23.0 → 0.24.1-canary.g56af4a4

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 (261) hide show
  1. package/README.md +1 -1
  2. package/bin/rebase-server.js +4 -2
  3. package/dist/GCSStorageController-DivE9bAx.js +402 -0
  4. package/dist/GCSStorageController-DivE9bAx.js.map +1 -0
  5. package/dist/S3StorageController-D_vKs4kP.js +480 -0
  6. package/dist/S3StorageController-D_vKs4kP.js.map +1 -0
  7. package/dist/api/ast-schema-editor.d.ts +92 -1
  8. package/dist/api/errors.d.ts +9 -0
  9. package/dist/api/live-schema-routes.d.ts +38 -8
  10. package/dist/api/logs-routes.d.ts +39 -1
  11. package/dist/api/openapi-generator.d.ts +17 -0
  12. package/dist/api/rest/api-generator.d.ts +97 -11
  13. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  14. package/dist/api/rest/field-access-query.d.ts +28 -8
  15. package/dist/api/rest/field-ops.d.ts +19 -7
  16. package/dist/api/rest/query-body.d.ts +10 -0
  17. package/dist/api/rest/write-validation.d.ts +9 -7
  18. package/dist/api/schema-editor-routes.d.ts +10 -1
  19. package/dist/api/sql-export-routes.d.ts +8 -0
  20. package/dist/api/types.d.ts +28 -1
  21. package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CSdL0iCk.js} +310 -12
  22. package/dist/ast-schema-editor-CSdL0iCk.js.map +1 -0
  23. package/dist/auth/access.d.ts +121 -0
  24. package/dist/auth/adapter-middleware.d.ts +2 -1
  25. package/dist/auth/address-ownership.d.ts +16 -1
  26. package/dist/auth/admin-roles-route.d.ts +4 -2
  27. package/dist/auth/admin-roles.d.ts +17 -20
  28. package/dist/auth/admin-user-ops.d.ts +24 -2
  29. package/dist/auth/admin-users-route.d.ts +2 -0
  30. package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
  31. package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
  32. package/dist/auth/api-keys/api-key-store.d.ts +31 -8
  33. package/dist/auth/api-keys/api-key-types.d.ts +14 -16
  34. package/dist/auth/api-keys/http-operation.d.ts +19 -0
  35. package/dist/auth/api-keys/index.d.ts +11 -11
  36. package/dist/auth/api-keys/key-grant.d.ts +41 -0
  37. package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
  38. package/dist/auth/auth-hooks.d.ts +46 -7
  39. package/dist/auth/builtin-auth-adapter.d.ts +8 -0
  40. package/dist/auth/cookie-utils.d.ts +7 -0
  41. package/dist/auth/deliverable-address.d.ts +6 -0
  42. package/dist/auth/email-change-routes.d.ts +41 -0
  43. package/dist/auth/expired-token-sweep.d.ts +71 -0
  44. package/dist/auth/impersonation.d.ts +110 -0
  45. package/dist/auth/index.d.ts +4 -2
  46. package/dist/auth/interfaces.d.ts +120 -59
  47. package/dist/auth/jwt-crypto.d.ts +7 -0
  48. package/dist/auth/jwt-keys.d.ts +4 -0
  49. package/dist/auth/jwt.d.ts +54 -3
  50. package/dist/auth/magic-link-routes.d.ts +2 -6
  51. package/dist/auth/mfa-routes.d.ts +2 -9
  52. package/dist/auth/middleware.d.ts +17 -5
  53. package/dist/auth/otp-routes.d.ts +2 -6
  54. package/dist/auth/passwordless-signup.d.ts +27 -0
  55. package/dist/auth/platform-token.d.ts +122 -0
  56. package/dist/auth/rate-limiter.d.ts +54 -1
  57. package/dist/auth/registration-policy.d.ts +17 -0
  58. package/dist/auth/routes.d.ts +45 -0
  59. package/dist/auth/scope-routes.d.ts +22 -0
  60. package/dist/auth/session-routes.d.ts +11 -6
  61. package/dist/auth/token-revocation.d.ts +50 -1
  62. package/dist/auth/user-picking.d.ts +35 -0
  63. package/dist/auth/verify-credential.d.ts +28 -0
  64. package/dist/{auth-B-GIMpDG.js → auth-OPq_FeMs.js} +3115 -638
  65. package/dist/auth-OPq_FeMs.js.map +1 -0
  66. package/dist/auth_adapter-CIItCTc8.js +21 -0
  67. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  68. package/dist/backend-DTAOsLQc.js.map +1 -1
  69. package/dist/backup/backup-common.d.ts +24 -3
  70. package/dist/backup/backup-routes.d.ts +46 -5
  71. package/dist/backup/backup-schedule.d.ts +33 -0
  72. package/dist/backup/backup-storage.d.ts +16 -0
  73. package/dist/backup/index.d.ts +5 -1
  74. package/dist/backup-5d7XhLX3.js +581 -0
  75. package/dist/backup-5d7XhLX3.js.map +1 -0
  76. package/dist/boot/bundle.d.ts +19 -0
  77. package/dist/boot/driver.d.ts +3 -0
  78. package/dist/boot/env.d.ts +56 -4
  79. package/dist/boot/http-server.d.ts +22 -0
  80. package/dist/boot/options.d.ts +16 -1
  81. package/dist/boot/runtime-cors.d.ts +17 -0
  82. package/dist/boot/security-headers.d.ts +26 -0
  83. package/dist/boot/static-routing.d.ts +65 -0
  84. package/dist/collection_patch-BRu-BvDv.js +472 -0
  85. package/dist/collection_patch-BRu-BvDv.js.map +1 -0
  86. package/dist/{contract-routes-CbFjuBwa.js → contract-routes-02lASyJI.js} +5 -8
  87. package/dist/contract-routes-02lASyJI.js.map +1 -0
  88. package/dist/cron/cron-routes.d.ts +1 -1
  89. package/dist/cron/cron-scheduler.d.ts +94 -23
  90. package/dist/cron/cron-store.d.ts +31 -4
  91. package/dist/cron/define-cron.d.ts +6 -2
  92. package/dist/cron/index.d.ts +1 -1
  93. package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-D7OmleAb.js} +11 -7
  94. package/dist/cron-loader-D7OmleAb.js.map +1 -0
  95. package/dist/{cron-routes-eE8nif_b.js → cron-routes-CBfiaqEL.js} +34 -8
  96. package/dist/cron-routes-CBfiaqEL.js.map +1 -0
  97. package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-C2vbETMo.js} +349 -65
  98. package/dist/cron-scheduler-C2vbETMo.js.map +1 -0
  99. package/dist/cron-store-B2Ih0gCN.js +540 -0
  100. package/dist/cron-store-B2Ih0gCN.js.map +1 -0
  101. package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
  102. package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
  103. package/dist/email/index.d.ts +2 -2
  104. package/dist/email/smtp-email-service.d.ts +23 -0
  105. package/dist/email/templates.d.ts +22 -0
  106. package/dist/email/types.d.ts +44 -0
  107. package/dist/env.d.ts +1 -39
  108. package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
  109. package/dist/errors-D6_y86c5.js.map +1 -0
  110. package/dist/filter-dialect-REl-TjZr.js +1628 -0
  111. package/dist/filter-dialect-REl-TjZr.js.map +1 -0
  112. package/dist/{function-loader-xnbDAPfa.js → function-loader-BDKKiBmy.js} +5 -4
  113. package/dist/function-loader-BDKKiBmy.js.map +1 -0
  114. package/dist/{function-routes-Chet4-lB.js → function-routes-DWB7BBsi.js} +25 -13
  115. package/dist/function-routes-DWB7BBsi.js.map +1 -0
  116. package/dist/functions/context.d.ts +26 -6
  117. package/dist/functions/define-function.d.ts +7 -4
  118. package/dist/functions/guards.d.ts +22 -5
  119. package/dist/functions/index.d.ts +2 -2
  120. package/dist/functions/index.js +114 -37
  121. package/dist/functions/index.js.map +1 -1
  122. package/dist/functions/request-timeout.d.ts +7 -5
  123. package/dist/history/history-routes.d.ts +9 -0
  124. package/dist/history/retention-sweep.d.ts +81 -0
  125. package/dist/{history-recorder-B4MpJfJK.js → history-recorder-DHenr3lm.js} +4 -4
  126. package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-DHenr3lm.js.map} +1 -1
  127. package/dist/{history-store-BhxWOuz9.js → history-store-DPofGVDp.js} +2 -2
  128. package/dist/{history-store-BhxWOuz9.js.map → history-store-DPofGVDp.js.map} +1 -1
  129. package/dist/index.d.ts +14 -7
  130. package/dist/index.es.js +23363 -17552
  131. package/dist/index.es.js.map +1 -1
  132. package/dist/init/health.d.ts +17 -2
  133. package/dist/init/shutdown.d.ts +20 -0
  134. package/dist/init.d.ts +70 -6
  135. package/dist/internal-tables-L2t39RdT.js +136 -0
  136. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  137. package/dist/jobs/job-queue.d.ts +8 -0
  138. package/dist/jobs/job-store.d.ts +24 -0
  139. package/dist/jobs/types.d.ts +25 -4
  140. package/dist/{jobs-CazMYhyy.js → jobs-BpD4Q0qR.js} +235 -25
  141. package/dist/jobs-BpD4Q0qR.js.map +1 -0
  142. package/dist/{jwt-DnQHNFCl.js → jwt-Bqcu7cGt.js} +207 -77
  143. package/dist/jwt-Bqcu7cGt.js.map +1 -0
  144. package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
  145. package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
  146. package/dist/load-failure-UvzZgGXT.js +48 -0
  147. package/dist/load-failure-UvzZgGXT.js.map +1 -0
  148. package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
  149. package/dist/logger-D-S-hO5e.js.map +1 -0
  150. package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
  151. package/dist/logs-routes-DAdv37GI.js.map +1 -0
  152. package/dist/mcp/consent-page.d.ts +1 -1
  153. package/dist/mcp/mcp-routes.d.ts +7 -0
  154. package/dist/mcp/mcp-tools.d.ts +23 -9
  155. package/dist/mcp/oauth-metadata.d.ts +21 -16
  156. package/dist/mcp/oauth-routes.d.ts +7 -1
  157. package/dist/metrics/index.d.ts +18 -0
  158. package/dist/metrics/pool-wait.d.ts +50 -0
  159. package/dist/offline-8Ks3yp9n-CadBKm2D.js +3305 -0
  160. package/dist/offline-8Ks3yp9n-CadBKm2D.js.map +1 -0
  161. package/dist/offline-store-D9ytNNaa-BCr2vsFB.js +1903 -0
  162. package/dist/offline-store-D9ytNNaa-BCr2vsFB.js.map +1 -0
  163. package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-BqoUh4Ij.js} +455 -80
  164. package/dist/openapi-generator-BqoUh4Ij.js.map +1 -0
  165. package/dist/policy-PdzX-Opd.js +121 -0
  166. package/dist/policy-PdzX-Opd.js.map +1 -0
  167. package/dist/project_manifest-R6y_KHms.js +59 -0
  168. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  169. package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
  170. package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
  171. package/dist/{query-parser-DGRVFNM3.js → query-parser-DK9eMkb9.js} +497 -52
  172. package/dist/query-parser-DK9eMkb9.js.map +1 -0
  173. package/dist/{request-timeout-C_4C2BeR.js → request-timeout-Dnl2_OBc.js} +17 -10
  174. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  175. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  176. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  177. package/dist/schema-edit/apply-schema-change.d.ts +73 -3
  178. package/dist/schema-edit/local-source.d.ts +29 -0
  179. package/dist/schema-edit/project-root.d.ts +3 -2
  180. package/dist/schema-edit/remote-source.d.ts +20 -5
  181. package/dist/schema-edit/source-editor.d.ts +28 -0
  182. package/dist/schema-editor-routes-BFf3i8jm.js +136 -0
  183. package/dist/schema-editor-routes-BFf3i8jm.js.map +1 -0
  184. package/dist/serve-spa.d.ts +58 -0
  185. package/dist/services/outbound-post.d.ts +50 -0
  186. package/dist/services/outbound-url-guard.d.ts +34 -5
  187. package/dist/services/routed-realtime-service.d.ts +16 -0
  188. package/dist/services/webhook-service.d.ts +66 -14
  189. package/dist/soft-delete-params-47yBGs3_.js +668 -0
  190. package/dist/soft-delete-params-47yBGs3_.js.map +1 -0
  191. package/dist/sql-export-routes-BNNuGGLP.js +240 -0
  192. package/dist/sql-export-routes-BNNuGGLP.js.map +1 -0
  193. package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
  194. package/dist/src-CatHFUym.js.map +1 -0
  195. package/dist/storage/GCSStorageController.d.ts +48 -2
  196. package/dist/storage/LocalStorageController.d.ts +69 -2
  197. package/dist/storage/S3StorageController.d.ts +39 -3
  198. package/dist/storage/image-transform.d.ts +5 -0
  199. package/dist/storage/index.d.ts +2 -2
  200. package/dist/storage/maintenance-routes.d.ts +23 -0
  201. package/dist/storage/maintenance.d.ts +126 -0
  202. package/dist/storage/property-limits.d.ts +41 -6
  203. package/dist/storage/rendition-cache.d.ts +9 -11
  204. package/dist/storage/request-keys.d.ts +15 -0
  205. package/dist/storage/request-principals.d.ts +30 -0
  206. package/dist/storage/requested-object.d.ts +74 -0
  207. package/dist/storage/routes.d.ts +52 -14
  208. package/dist/storage/source-variables.d.ts +10 -0
  209. package/dist/storage/streams.d.ts +42 -0
  210. package/dist/storage/tus-handler.d.ts +76 -7
  211. package/dist/storage/types.d.ts +101 -7
  212. package/dist/storage/upload-validation.d.ts +13 -0
  213. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  214. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  215. package/dist/strings.es-BeZWAodn.js +256 -0
  216. package/dist/strings.es-BeZWAodn.js.map +1 -0
  217. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  218. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  219. package/dist/utils/byte-size.d.ts +10 -0
  220. package/dist/utils/compression.d.ts +6 -0
  221. package/dist/utils/load-failure.d.ts +14 -0
  222. package/dist/utils/logger.d.ts +12 -0
  223. package/package.json +5 -5
  224. package/dist/GCSStorageController-CjrA4PMo.js +0 -242
  225. package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
  226. package/dist/S3StorageController-B6pKDNVj.js +0 -264
  227. package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
  228. package/dist/admin-roles-vYdp_Pil.js +0 -36
  229. package/dist/admin-roles-vYdp_Pil.js.map +0 -1
  230. package/dist/admin_block-DxKLmdiv.js +0 -206
  231. package/dist/admin_block-DxKLmdiv.js.map +0 -1
  232. package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
  233. package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
  234. package/dist/auth-B-GIMpDG.js.map +0 -1
  235. package/dist/backup-D7YR94N3.js +0 -253
  236. package/dist/backup-D7YR94N3.js.map +0 -1
  237. package/dist/contract-routes-CbFjuBwa.js.map +0 -1
  238. package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
  239. package/dist/cron-routes-eE8nif_b.js.map +0 -1
  240. package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
  241. package/dist/cron-store-TcoGz-xS.js +0 -305
  242. package/dist/cron-store-TcoGz-xS.js.map +0 -1
  243. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  244. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  245. package/dist/errors-DWsX4yTd.js.map +0 -1
  246. package/dist/function-loader-xnbDAPfa.js.map +0 -1
  247. package/dist/function-routes-Chet4-lB.js.map +0 -1
  248. package/dist/jobs-CazMYhyy.js.map +0 -1
  249. package/dist/jwt-DnQHNFCl.js.map +0 -1
  250. package/dist/logger-DO2PZc4i.js.map +0 -1
  251. package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
  252. package/dist/openapi-generator-O_O24MAT.js.map +0 -1
  253. package/dist/query-parser-DGRVFNM3.js.map +0 -1
  254. package/dist/request-timeout-C_4C2BeR.js.map +0 -1
  255. package/dist/schema-editor-routes-C5-lh_jO.js +0 -87
  256. package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
  257. package/dist/src-pmvW7BFx.js +0 -6688
  258. package/dist/src-pmvW7BFx.js.map +0 -1
  259. package/dist/src-vkcwKXbT.js.map +0 -1
  260. package/dist/types-BfKcm9do.js +0 -39
  261. package/dist/types-BfKcm9do.js.map +0 -1
@@ -2,58 +2,260 @@ import { createRequire as __rebaseCreateRequire } from "module";
2
2
  import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
- import { D as restrictedFieldNames, G as resolveCollectionRelations, S as CursorMismatchError, T as reconcileCursorOrder, a as deserializeFilter, f as IncludeSpecError, h as normalizeInclude, i as UnknownFilterOperatorError, o as deserializeLogicalCondition, p as deserializeInclude, r as RESERVED_QUERY_KEYS, rt as resolveClientListLimit, tt as ListLimitError, v as OrderBySpecError, w as decodeCursor, x as CursorError } from "./src-pmvW7BFx.js";
6
- import "./src-vkcwKXbT.js";
7
- import { t as ApiError } from "./errors-DWsX4yTd.js";
8
- //#region src/api/rest/soft-delete-params.ts
5
+ import { i as ListLimitError, o as resolveClientListLimit } from "./strings.es-BeZWAodn.js";
6
+ import { d as IncludeSpecError, f as deserializeInclude, g as OrderBySpecError, i as deserializeLogicalCondition, m as normalizeInclude, n as UnknownFilterOperatorError, r as deserializeFilter, s as PAGE_WITH_OFFSET_MESSAGE, t as RESERVED_QUERY_KEYS } from "./filter-dialect-REl-TjZr.js";
7
+ import { $ as parseRelationAggregateSort } from "./src-CatHFUym.js";
8
+ import { a as apiHiddenFieldNames, c as fieldKeyForColumn, i as parseWithDeleted, l as findRelation, m as resolveCollectionRelations, s as restrictedFieldNames, t as DELETED_QUERY_PARAM } from "./soft-delete-params-47yBGs3_.js";
9
+ import { t as ApiError } from "./errors-D6_y86c5.js";
10
+ //#region ../common/src/collections/unknown-field.ts
9
11
  /**
10
- * The two query parameters soft delete adds to the REST surface.
12
+ * The fields an unknown-field answer offers as valid, sorted: what a query on
13
+ * this collection can name as a column. A relation contributes its foreign key
14
+ * (`authorId`) when the key is on this table, and nothing otherwise. An
15
+ * `excludeFromApi` field is never offered.
11
16
  *
12
- * Kept in their own module so the call sites in `query-parser.ts` and the
13
- * delete routes are a single line each: the parsing rules belong to soft
14
- * delete, not to the parser, and a rule spread across the two files that read
15
- * it is a rule that drifts.
17
+ * Read off the collection rather than the table, so the REST check — which
18
+ * holds no table — lists exactly what the driver lists.
16
19
  */
17
- /** `?deleted=` — what to do about rows a soft delete has stamped. */
18
- var DELETED_QUERY_PARAM = "deleted";
19
- /** `?hard=` — ask for a real `DELETE` on a soft-delete collection. */
20
- var HARD_DELETE_QUERY_PARAM = "hard";
20
+ function queryableFieldNames(collection) {
21
+ const hidden = apiHiddenFieldNames(collection);
22
+ const names = /* @__PURE__ */ new Set();
23
+ let relations;
24
+ for (const [key, property] of Object.entries(collection.properties ?? {})) {
25
+ if (property?.type === "relation") {
26
+ relations ??= resolveCollectionRelations(collection);
27
+ const relation = findRelation(relations, key);
28
+ if (relation?.kind === "belongsTo") names.add(fieldKeyForColumn(collection, relation.localKey));
29
+ continue;
30
+ }
31
+ names.add(key);
32
+ }
33
+ return [...names].filter((name) => !hidden.has(name)).sort();
34
+ }
35
+ var validList = (validFields) => validFields.length > 0 ? `. Valid fields: ${validFields.join(", ")}` : "";
36
+ /** A filter key that resolves to nothing on `collectionPath`. */
37
+ function unknownFilterField(field, collectionPath, validFields) {
38
+ return {
39
+ message: `Unknown filter field '${field}' on collection '${collectionPath}'${validList(validFields)}`,
40
+ code: "UNKNOWN_FILTER_FIELD",
41
+ details: {
42
+ field,
43
+ collection: collectionPath,
44
+ ...validFields.length > 0 && { validFields }
45
+ }
46
+ };
47
+ }
48
+ /** `author.emial` — a relation path whose last segment names nothing on the relation's target. */
49
+ function unknownRelationTargetField(path, target, collectionPath) {
50
+ return {
51
+ message: `Unknown field '${path.fieldKey}' on '${target.slug}', the target of relation '${path.relationKey}' on collection '${collectionPath}'${validList(target.validFields)}`,
52
+ code: "UNKNOWN_FILTER_FIELD",
53
+ details: {
54
+ field: path.field,
55
+ collection: collectionPath,
56
+ relation: path.relationKey,
57
+ targetCollection: target.slug,
58
+ ...target.validFields.length > 0 && { validFields: target.validFields }
59
+ }
60
+ };
61
+ }
62
+ /** An `orderBy` key that resolves to nothing. `collectionName` is absent when the read carries no collection. */
63
+ function unknownOrderByField(orderBy, collectionName, validFields) {
64
+ return {
65
+ message: `Unknown orderBy field '${orderBy}'${collectionName ? ` on collection '${collectionName}'` : ""}${validList(validFields)}`,
66
+ code: "UNKNOWN_ORDER_BY_FIELD",
67
+ details: {
68
+ field: orderBy,
69
+ ...collectionName && { collection: collectionName },
70
+ ...validFields.length > 0 && { validFields }
71
+ }
72
+ };
73
+ }
74
+ /** A `fields` entry that names nothing. */
75
+ function unknownProjectionField(name, collectionName, validFields) {
76
+ return {
77
+ message: `Unknown field '${name}' in \`fields\` on collection '${collectionName}'. Valid fields: ${validFields.join(", ")}`,
78
+ code: "UNKNOWN_FIELD",
79
+ details: {
80
+ field: name,
81
+ collection: collectionName
82
+ }
83
+ };
84
+ }
85
+ /**
86
+ * An `include` naming no relation of the collection. `validRelations` is what
87
+ * the collection declares, never an `excludeFromApi` relation.
88
+ */
89
+ function unknownIncludeRelation(key, collectionName, validRelations) {
90
+ return {
91
+ message: `Unknown relation '${key}' on collection '${collectionName}'` + (validRelations.length > 0 ? `. Its relations are: ${validRelations.join(", ")}` : ". It declares no relations."),
92
+ code: "UNKNOWN_RELATION",
93
+ details: {
94
+ relation: key,
95
+ collection: collectionName,
96
+ ...validRelations.length > 0 && { validRelations }
97
+ }
98
+ };
99
+ }
100
+ /** The relations an unknown-relation answer offers, sorted — never an `excludeFromApi` one. */
101
+ function includableRelationNames(collection) {
102
+ const hidden = apiHiddenFieldNames(collection);
103
+ return Object.keys(resolveCollectionRelations(collection)).filter((key) => !hidden.has(key)).sort();
104
+ }
21
105
  /**
22
- * `?deleted=include|only` → the driver's `withDeleted`.
106
+ * An aggregate's `select` field or `groupBy` key that names no column.
107
+ * `forWhat` is where it was named: `sum()`, `count()`, `groupBy`.
108
+ * `validFields` is {@link queryableFieldNames}, never an `excludeFromApi` field.
109
+ */
110
+ function unknownAggregateField(field, forWhat, validFields) {
111
+ return {
112
+ message: `Unknown field '${field}' in ${forWhat}${validList(validFields)}`,
113
+ code: "UNKNOWN_AGGREGATE_FIELD",
114
+ details: {
115
+ field,
116
+ ...validFields.length > 0 && { validFields }
117
+ }
118
+ };
119
+ }
120
+ /**
121
+ * The properties a `vector_search` can name, sorted: those declared
122
+ * `{ type: "vector" }`, never an `excludeFromApi` one.
123
+ */
124
+ function vectorPropertyNames(collection) {
125
+ const hidden = apiHiddenFieldNames(collection);
126
+ return Object.entries(collection.properties ?? {}).filter(([key, property]) => property?.type === "vector" && !hidden.has(key)).map(([key]) => key).sort();
127
+ }
128
+ /** A `vector_search` naming no vector property. `vectorProperties` is {@link vectorPropertyNames}. */
129
+ function unknownVectorProperty(property, vectorProperties) {
130
+ return {
131
+ message: `Unknown vector property "${property}". ` + (vectorProperties.length > 0 ? `This collection's vector properties are: ${vectorProperties.join(", ")}.` : "This collection declares no `vector` property to search."),
132
+ code: "UNKNOWN_VECTOR_PROPERTY",
133
+ details: {
134
+ property,
135
+ ...vectorProperties.length > 0 && { vectorProperties }
136
+ }
137
+ };
138
+ }
139
+ //#endregion
140
+ //#region ../common/src/data/cursor.ts
141
+ /** A cursor that cannot be read at all — truncated, re-encoded, or invented. */
142
+ var CursorError = class CursorError extends Error {
143
+ code = "INVALID_CURSOR";
144
+ constructor(detail) {
145
+ super(`Invalid \`after\` cursor: ${detail}. Pass back the \`meta.nextCursor\` from the previous page unchanged — it is opaque and must not be built by hand.`);
146
+ this.name = "CursorError";
147
+ Object.setPrototypeOf(this, CursorError.prototype);
148
+ }
149
+ };
150
+ /**
151
+ * A cursor that reads fine but describes a different query.
23
152
  *
24
- * Spelled `deleted` on the wire and `withDeleted` in the driver, deliberately:
25
- * the URL reads as a question about the rows (`?deleted=only` — "only the
26
- * deleted ones"), and the driver option reads as an instruction about the query.
153
+ * Separate from {@link CursorError} because the fix is different: this one is
154
+ * not a corrupt string, it is a correct cursor used against a sort it was not
155
+ * produced under. Seeking anyway would return rows in an order nobody asked
156
+ * for, and — worse — would look like it worked.
157
+ */
158
+ var CursorMismatchError = class CursorMismatchError extends Error {
159
+ code = "CURSOR_ORDER_MISMATCH";
160
+ constructor(cursorKeys, queryKeys) {
161
+ super(`The \`after\` cursor was produced by a query ordered by ${cursorKeys.map((k) => `"${k}"`).join(", ") || "(nothing)"}, but this query orders by ${queryKeys.map((k) => `"${k}"`).join(", ") || "(nothing)"}. A cursor only continues the listing it came from — keep \`orderBy\` identical across pages, or drop \`after\` to start over.`);
162
+ this.name = "CursorMismatchError";
163
+ Object.setPrototypeOf(this, CursorMismatchError.prototype);
164
+ }
165
+ };
166
+ /**
167
+ * Tag for a value whose JSON round-trip would otherwise lose its type.
27
168
  *
28
- * A value neither word is a 400 rather than a silent fallback to the default.
29
- * `?deleted=true` quietly hiding every deleted row is the worst of both: it
30
- * looks like it worked and answers the opposite question. Absent is the
31
- * default, which is "hide them".
169
+ * A `timestamp` column comes back from the driver as a `Date`; JSON turns it
170
+ * into a string, and the string would then be compared against the column by
171
+ * whatever cast Postgres chose. Round-tripping it as a `Date` keeps the
172
+ * comparison the one the ORDER BY made.
32
173
  */
33
- function parseWithDeleted(raw) {
34
- if (raw === void 0 || raw === null || raw === "") return void 0;
35
- const value = String(raw).trim().toLowerCase();
36
- if (value === "include") return true;
37
- if (value === "only") return "only";
38
- throw ApiError.badRequest(`Invalid \`?${DELETED_QUERY_PARAM}=${String(raw)}\`. It takes 'include' (live rows and deleted ones) or 'only' (deleted rows alone). Omit it to see only the live rows.`, "INVALID_DELETED_PARAM");
174
+ var DATE_TAG = "$date";
175
+ function decodeValue(value) {
176
+ if (value && typeof value === "object" && !Array.isArray(value)) {
177
+ const tagged = value[DATE_TAG];
178
+ if (typeof tagged === "string") {
179
+ const date = new Date(tagged);
180
+ return Number.isNaN(date.getTime()) ? tagged : date;
181
+ }
182
+ }
183
+ return value;
184
+ }
185
+ function fromBase64Url(encoded) {
186
+ const padded = encoded.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - encoded.length % 4) % 4);
187
+ const binary = atob(padded);
188
+ const bytes = new Uint8Array(binary.length);
189
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
190
+ return new TextDecoder().decode(bytes);
191
+ }
192
+ /**
193
+ * Read a cursor produced by {@link encodeCursor}.
194
+ *
195
+ * @throws {CursorError} when the string is not a cursor this codec wrote.
196
+ */
197
+ function decodeCursor(raw) {
198
+ let parsed;
199
+ try {
200
+ parsed = JSON.parse(fromBase64Url(raw.trim()));
201
+ } catch {
202
+ throw new CursorError("it is not a cursor this API issued");
203
+ }
204
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new CursorError("it does not decode to a cursor");
205
+ const body = parsed;
206
+ if (!Array.isArray(body.k)) throw new CursorError("it carries no sort keys");
207
+ if (body.i === void 0) throw new CursorError("it carries no row id");
208
+ const orderBy = [];
209
+ for (const entry of body.k) {
210
+ if (!Array.isArray(entry) || typeof entry[0] !== "string") throw new CursorError("one of its sort keys is malformed");
211
+ const direction = entry[1] === "desc" ? "desc" : "asc";
212
+ orderBy.push(entry[2] === "first" || entry[2] === "last" ? [
213
+ entry[0],
214
+ direction,
215
+ entry[2]
216
+ ] : [entry[0], direction]);
217
+ }
218
+ const rawValues = body.v && typeof body.v === "object" && !Array.isArray(body.v) ? body.v : {};
219
+ const values = {};
220
+ for (const [field, value] of Object.entries(rawValues)) values[field] = decodeValue(value);
221
+ return {
222
+ orderBy,
223
+ values,
224
+ id: decodeValue(body.i)
225
+ };
39
226
  }
40
227
  /**
41
- * `?hard=true` → a real `DELETE` on a collection that soft-deletes.
228
+ * The `orderBy` a request should run under, given a cursor and whatever sort
229
+ * the request itself named.
42
230
  *
43
- * Needs no permission beyond the delete it replaces: it is the same verb, and a
44
- * second access-control surface for one operation is a second thing to get
45
- * wrong. What it changes is whether the row can be restored.
231
+ * A request that names no sort **adopts the cursor's** — that is what makes
232
+ * `find({ after })` work without restating the `orderBy` from the previous
233
+ * call, and it cannot be wrong, since the cursor is the only sort in play.
234
+ * A request that names one must name the *same* one, key for key, direction for
235
+ * direction, nulls for nulls; anything else is {@link CursorMismatchError}.
46
236
  *
47
- * Only the exact words `true` and `1` mean yes. Anything else is a 400, not a
48
- * "no" — a typo that silently soft-deletes when the caller asked to purge is a
49
- * caller who believes the data is gone.
237
+ * @throws {CursorMismatchError}
50
238
  */
51
- function parseHardDelete(raw) {
52
- if (raw === void 0 || raw === null || raw === "") return false;
53
- const value = String(raw).trim().toLowerCase();
54
- if (value === "true" || value === "1") return true;
55
- if (value === "false" || value === "0") return false;
56
- throw ApiError.badRequest(`Invalid \`?${HARD_DELETE_QUERY_PARAM}=${String(raw)}\`. It takes 'true' or 'false'.`, "INVALID_HARD_PARAM");
239
+ function reconcileCursorOrder(cursor, requested) {
240
+ if (!requested || requested.length === 0) return cursor.orderBy;
241
+ const spell = (keys) => keys.map(([field, direction, nulls]) => `${field}:${direction}${nulls ? `:${nulls}` : ""}`);
242
+ const cursorKeys = spell(cursor.orderBy);
243
+ const queryKeys = spell(requested);
244
+ if (cursorKeys.length !== queryKeys.length || cursorKeys.some((key, i) => key !== queryKeys[i])) throw new CursorMismatchError(cursorKeys, queryKeys);
245
+ return requested;
246
+ }
247
+ /**
248
+ * The `startAfter` shape the driver contract takes, built from a cursor.
249
+ *
250
+ * The driver has always accepted `{ id, values }`; this is the one place that
251
+ * shape is produced, so the REST route and the WebSocket ingress cannot drift
252
+ * into two spellings of the same seek.
253
+ */
254
+ function cursorToStartAfter(cursor) {
255
+ return {
256
+ id: cursor.id,
257
+ values: cursor.values
258
+ };
57
259
  }
58
260
  //#endregion
59
261
  //#region src/api/rest/field-access-query.ts
@@ -66,13 +268,22 @@ function parseHardDelete(raw) {
66
268
  * `?orderBy=salary` returns them in order of it. A column no response can carry
67
269
  * has to be a column no query can interrogate, or the read rule is decoration.
68
270
  *
69
- * The refusal names the field. That is deliberate and it is not a leak: the
70
- * published OpenAPI lists every property of every collection, including the ones
71
- * a given caller cannot read, because the document is one document and is served
72
- * off the app rather than off the authenticated data router. Hiding the name
73
- * here would protect nothing and would answer a caller's genuine typo with
74
- * "unknown field", sending them to look for a spelling mistake that is not
75
- * there. Field *names* are public; field *values* are not.
271
+ * The refusal names a field restricted by role. That is deliberate and it is
272
+ * not a leak: the published OpenAPI lists every property of every collection,
273
+ * including the ones a given caller cannot read, because the document is one
274
+ * document and is served off the app rather than off the authenticated data
275
+ * router. Hiding the name here would protect nothing and would answer a
276
+ * caller's genuine typo with "unknown field", sending them to look for a
277
+ * spelling mistake that is not there. Those field *names* are public; their
278
+ * *values* are not.
279
+ *
280
+ * An `excludeFromApi` field is the exception, because its name is *not*
281
+ * public: OpenAPI, `list_collections` and the generated SDK all leave it out.
282
+ * "`email` is not readable on `authors`" would confirm the one thing those hide,
283
+ * so a filter, sort, projection, include, aggregate or vector search naming one
284
+ * is answered as one naming a field that does not exist — built by the functions the driver answers a
285
+ * typo with (`@rebasepro/common`'s `unknown-field`), so the two answers are the
286
+ * same bytes. See {@link assertNoHiddenNames}.
76
287
  *
77
288
  * @module
78
289
  */
@@ -136,7 +347,8 @@ function assertReadableFields(names, collection, viewer, where) {
136
347
  if (names.length === 0) return;
137
348
  const { refused } = restrictedFieldNames(collection, viewer, "read");
138
349
  if (refused.size === 0) return;
139
- const named = [...new Set(names.filter((n) => n !== void 0 && refused.has(n)))];
350
+ const hidden = apiHiddenFieldNames(collection);
351
+ const named = [...new Set(names.filter((n) => n !== void 0 && refused.has(n) && !hidden.has(n)))];
140
352
  if (named.length === 0) return;
141
353
  throw ApiError.badRequest(`${named.map((f) => `'${f}'`).join(", ")} ${named.length > 1 ? "are" : "is"} not readable on '${collection.slug}' with your roles, so ${named.length > 1 ? "they" : "it"} cannot be used in ${WHERE_LABEL[where]}.`, "FIELD_NOT_READABLE", {
142
354
  collection: collection.slug,
@@ -156,15 +368,240 @@ function assertReadableFields(names, collection, viewer, where) {
156
368
  * parsed and then dropped by the list route.
157
369
  */
158
370
  function assertQueryFieldsReadable(options, collection, viewer) {
371
+ const filtered = assertRestrictedFieldsUnused(options, collection, viewer);
372
+ const include = options.include !== void 0 ? normalizeInclude(options.include) : void 0;
373
+ assertNoHiddenNames({
374
+ ...options,
375
+ filtered,
376
+ include: include?.tree
377
+ }, collection);
378
+ }
379
+ /**
380
+ * The aggregate route's `select` and `groupBy`, which it parses itself: an
381
+ * `excludeFromApi` name there is answered exactly as the driver answers a
382
+ * column that does not exist. Walked in the driver's order — each aggregate's
383
+ * field, then each group key — and the first hidden *or* unknown name gets the
384
+ * answer, so a typo before a hidden name is still the one named.
385
+ */
386
+ function assertNoHiddenAggregateNames(aggregates, groupBy, collection) {
387
+ const hidden = apiHiddenFieldNames(collection);
388
+ if (hidden.size === 0) return;
389
+ const asked = [...aggregates.flatMap(({ fn, field }) => field === void 0 ? [] : [{
390
+ name: field,
391
+ forWhat: `${fn}()`
392
+ }]), ...groupBy.map((name) => ({
393
+ name,
394
+ forWhat: "groupBy"
395
+ }))];
396
+ if (!asked.some(({ name }) => hidden.has(name))) return;
397
+ const valid = queryableFieldNames(collection);
398
+ const known = (name) => !hidden.has(name) && (valid.includes(name) || name in (collection.properties ?? {}));
399
+ for (const { name, forWhat } of asked) {
400
+ if (known(name)) continue;
401
+ const answer = unknownAggregateField(name, forWhat, valid);
402
+ throw ApiError.badRequest(answer.message, answer.code, answer.details);
403
+ }
404
+ }
405
+ /**
406
+ * Refuse every field restricted by role that the read names, at every level of
407
+ * its include tree. Returns the filter's column names, for the hidden pass.
408
+ */
409
+ function assertRestrictedFieldsUnused(options, collection, viewer) {
159
410
  const filtered = [];
160
411
  if (options.where) filtered.push(...Object.keys(options.where));
161
412
  logicalColumns(options.logical, filtered);
162
- assertReadableFields(filtered, collection, viewer, "filter");
413
+ assertReadableFields(filtered.map(filterHead), collection, viewer, "filter");
414
+ assertRelationPathsReadable(filtered, collection, viewer);
163
415
  assertReadableFields((options.orderBy ?? []).map((entry) => orderByColumn(entry.field)), collection, viewer, "orderBy");
416
+ assertAggregateSortsReadable(options.orderBy ?? [], collection, viewer);
164
417
  assertReadableFields(options.fields ?? [], collection, viewer, "fields");
165
418
  assertReadableFields(options.vectorSearch ? [options.vectorSearch.property] : [], collection, viewer, "vector_search");
166
419
  const include = options.include !== void 0 ? normalizeInclude(options.include) : void 0;
167
420
  if (include) assertIncludeFieldsReadable(include.tree, collection, viewer);
421
+ return filtered;
422
+ }
423
+ /**
424
+ * The field a filter key reads on *this* collection: the column before a JSON
425
+ * path's first arrow (`metadata->theme` reads `metadata`), the relation before
426
+ * a relation path's dot (`author.email` reads `author`), the key itself
427
+ * otherwise. A key that is a property outright wins, as the driver resolves it.
428
+ */
429
+ function filterHead(name) {
430
+ const arrow = name.indexOf("->");
431
+ if (arrow > 0) return name.slice(0, arrow);
432
+ const dot = name.indexOf(".");
433
+ if (dot > 0) return name.slice(0, dot);
434
+ return name;
435
+ }
436
+ /**
437
+ * `orderBy=max(staff.salary)` ranks the parents by the target's `salary` — the
438
+ * same reading `orderBy=salary` on `staff` is — so the aggregated field is
439
+ * judged by the target's rule. Whether the relation itself may be read is the
440
+ * driver's check (`assertRelationReadable`), which runs on every door.
441
+ */
442
+ function assertAggregateSortsReadable(orderBy, collection, viewer) {
443
+ let relations;
444
+ for (const { field } of orderBy) {
445
+ const spec = parseRelationAggregateSort(field);
446
+ if (!spec?.field) continue;
447
+ relations ??= resolveCollectionRelations(collection);
448
+ const relation = findRelation(relations, spec.relation);
449
+ if (!relation) continue;
450
+ assertReadableFields([spec.field], relation.target(), viewer, "orderBy");
451
+ }
452
+ }
453
+ /**
454
+ * `?author.salary=gt.100000` reads the target's `salary`, a row at a time, as
455
+ * `?salary=gt.100000` on `authors` would — so it is judged by the target's
456
+ * rule. Only a path whose first segment is a relation here; anything else is
457
+ * the driver's to refuse.
458
+ */
459
+ function assertRelationPathsReadable(filtered, collection, viewer) {
460
+ let relations;
461
+ for (const name of filtered) {
462
+ const dot = name.indexOf(".");
463
+ if (dot <= 0 || name.indexOf("->") >= 0) continue;
464
+ relations ??= resolveCollectionRelations(collection);
465
+ const relation = findRelation(relations, name.slice(0, dot));
466
+ if (!relation) continue;
467
+ assertReadableFields([name.slice(dot + 1)], relation.target(), viewer, "filter");
468
+ }
469
+ }
470
+ /**
471
+ * Answer a read naming an `excludeFromApi` field — or relation — exactly as the
472
+ * driver answers a read naming a field that does not exist.
473
+ *
474
+ * The names are walked in the order the driver resolves them — the `fields`
475
+ * projection, then the filter, then the sort, then each include, parent before
476
+ * child — and the first that is hidden *or* unknown gets the answer. Answering
477
+ * only the hidden one would still tell the two apart: with `?aaa=x&email=x` the
478
+ * driver names `aaa` when `email` does not exist, so this has to as well.
479
+ *
480
+ * A request naming nothing hidden is left alone entirely, so the driver stays
481
+ * the only one answering ordinary typos.
482
+ */
483
+ function assertNoHiddenNames(options, collection) {
484
+ const hidden = apiHiddenFieldNames(collection);
485
+ if (hidden.size > 0) {
486
+ const shapes = [
487
+ ...(options.fields ?? []).map((name) => ({
488
+ kind: "fields",
489
+ name
490
+ })),
491
+ ...options.filtered.map((name) => ({
492
+ kind: "filter",
493
+ name
494
+ })),
495
+ ...(options.orderBy ?? []).flatMap((entry) => {
496
+ const name = orderByColumn(entry.field);
497
+ return name === void 0 ? [] : [{
498
+ kind: "orderBy",
499
+ name
500
+ }];
501
+ })
502
+ ].map(({ kind, name }) => ({
503
+ kind,
504
+ name,
505
+ shape: nameShape(name, collection, hidden)
506
+ }));
507
+ const vectorHidden = options.vectorSearch !== void 0 && hidden.has(options.vectorSearch.property);
508
+ if (vectorHidden || shapes.some((entry) => entry.shape.hidden)) for (const { kind, name, shape } of shapes) {
509
+ if (shape.known && !shape.hidden) continue;
510
+ const answer = unknownAnswer(kind, name, shape, collection);
511
+ throw ApiError.badRequest(answer.message, answer.code, answer.details);
512
+ }
513
+ if (vectorHidden && options.vectorSearch) {
514
+ const answer = unknownVectorProperty(options.vectorSearch.property, vectorPropertyNames(collection));
515
+ throw ApiError.badRequest(answer.message, answer.code, answer.details);
516
+ }
517
+ }
518
+ const relations = resolveCollectionRelations(collection);
519
+ for (const [key, node] of Object.entries(options.include ?? {})) {
520
+ if (hidden.has(key)) {
521
+ const answer = unknownIncludeRelation(key, collection.slug ?? collection.name, includableRelationNames(collection));
522
+ throw ApiError.badRequest(answer.message, answer.code, answer.details);
523
+ }
524
+ const relation = relations[key];
525
+ if (!relation) continue;
526
+ const filtered = [];
527
+ if (node.where) filtered.push(...Object.keys(node.where));
528
+ logicalColumns(node.logical, filtered);
529
+ assertNoHiddenNames({
530
+ filtered,
531
+ orderBy: node.orderBy?.map(([field]) => ({ field })),
532
+ fields: node.fields,
533
+ include: node.children
534
+ }, relation.target());
535
+ }
536
+ }
537
+ function nameShape(name, collection, hidden) {
538
+ if (hidden.has(name)) return {
539
+ hidden: true,
540
+ known: false
541
+ };
542
+ const columns = new Set(queryableFieldNames(collection));
543
+ const relations = resolveCollectionRelations(collection);
544
+ if (columns.has(name) || name in (collection.properties ?? {})) return {
545
+ hidden: false,
546
+ known: true
547
+ };
548
+ const arrow = name.indexOf("->");
549
+ if (arrow > 0) {
550
+ const head = name.slice(0, arrow);
551
+ return {
552
+ hidden: hidden.has(head),
553
+ known: columns.has(head)
554
+ };
555
+ }
556
+ const dot = name.indexOf(".");
557
+ if (dot > 0) {
558
+ const relationKey = name.slice(0, dot);
559
+ const fieldKey = name.slice(dot + 1);
560
+ if (hidden.has(relationKey)) return {
561
+ hidden: true,
562
+ known: false
563
+ };
564
+ const relation = findRelation(relations, relationKey);
565
+ if (!relation) return {
566
+ hidden: false,
567
+ known: false
568
+ };
569
+ const target = relation.target();
570
+ const targetHidden = apiHiddenFieldNames(target);
571
+ const path = {
572
+ relationKey,
573
+ fieldKey,
574
+ target
575
+ };
576
+ if (targetHidden.has(fieldKey)) return {
577
+ hidden: true,
578
+ known: false,
579
+ path
580
+ };
581
+ return {
582
+ hidden: false,
583
+ known: queryableFieldNames(target).includes(fieldKey) || fieldKey in (target.properties ?? {}),
584
+ path
585
+ };
586
+ }
587
+ return {
588
+ hidden: false,
589
+ known: false
590
+ };
591
+ }
592
+ function unknownAnswer(kind, name, shape, collection) {
593
+ const slug = collection.slug ?? collection.name;
594
+ if (kind === "fields") return unknownProjectionField(name, slug, queryableFieldNames(collection));
595
+ if (kind === "orderBy") return unknownOrderByField(name, slug, queryableFieldNames(collection));
596
+ if (shape.path) return unknownRelationTargetField({
597
+ field: name,
598
+ relationKey: shape.path.relationKey,
599
+ fieldKey: shape.path.fieldKey
600
+ }, {
601
+ slug: shape.path.target.slug,
602
+ validFields: queryableFieldNames(shape.path.target)
603
+ }, slug);
604
+ return unknownFilterField(name, slug, queryableFieldNames(collection));
168
605
  }
169
606
  /**
170
607
  * Every relation an `include` loads, judged like the read it is part of.
@@ -186,7 +623,7 @@ function assertIncludeFieldsReadable(tree, collection, viewer) {
186
623
  const relation = relations[key];
187
624
  if (!relation) continue;
188
625
  const target = relation.target();
189
- assertQueryFieldsReadable({
626
+ assertRestrictedFieldsUnused({
190
627
  where: node.where,
191
628
  logical: node.logical,
192
629
  orderBy: node.orderBy?.map(([field]) => ({ field })),
@@ -263,6 +700,7 @@ function parseLogicalGroup(type, raw) {
263
700
  try {
264
701
  parsed = deserializeLogicalCondition(`${type}(${inner})`);
265
702
  } catch (e) {
703
+ if (e instanceof UnknownFilterOperatorError) throw invalidParam(e.message, e.code, e.details);
266
704
  throw invalidParam(`Invalid \`${type}\` parameter: ${e instanceof Error ? e.message : String(e)}`, "INVALID_LOGICAL_GROUP");
267
705
  }
268
706
  return "type" in parsed ? parsed : void 0;
@@ -514,6 +952,7 @@ function parseQueryOptions(query, limits = {}, access) {
514
952
  const offsetVal = getLastValue(query.offset);
515
953
  if (offsetVal) options.offset = parseWindowParam(offsetVal, "offset", 0, "INVALID_OFFSET");
516
954
  const pageVal = getLastValue(query.page);
955
+ if (pageVal && offsetVal) throw invalidParam(PAGE_WITH_OFFSET_MESSAGE, "PAGE_WITH_OFFSET");
517
956
  if (pageVal) {
518
957
  const page = parseWindowParam(pageVal, "page", 1, "INVALID_PAGE");
519
958
  const limit = resolveListLimitParam(rawLimit, {
@@ -566,6 +1005,12 @@ function parseQueryOptions(query, limits = {}, access) {
566
1005
  if (text !== "true" && text !== "1" && text !== "false" && text !== "0") throw invalidParam(`Invalid \`distinct\` parameter: expected \`true\` or \`false\`, got ${JSON.stringify(String(distinctVal))}.`, "INVALID_DISTINCT");
567
1006
  options.distinct = text === "true" || text === "1";
568
1007
  }
1008
+ const countVal = getLastValue(query.count);
1009
+ if (countVal !== void 0 && countVal !== null && String(countVal) !== "") {
1010
+ const text = String(countVal).trim().toLowerCase();
1011
+ if (text !== "exact" && text !== "none") throw invalidParam(`Invalid \`count\` parameter: expected \`exact\` or \`none\`, got ${JSON.stringify(String(countVal))}.`, "INVALID_COUNT");
1012
+ options.count = text;
1013
+ }
569
1014
  const afterVal = getLastValue(query.after);
570
1015
  if (afterVal !== void 0 && afterVal !== null && String(afterVal).trim() !== "") {
571
1016
  let cursor;
@@ -628,6 +1073,6 @@ function parseQueryOptions(query, limits = {}, access) {
628
1073
  return options;
629
1074
  }
630
1075
  //#endregion
631
- export { resolveListLimitParam as a, requestViewer as c, parseQueryOptions as i, HARD_DELETE_QUERY_PARAM as l, parseAggregateSelect as n, assertQueryFieldsReadable as o, parseGroupBy as r, assertReadableFields as s, orderByEntriesToTuples as t, parseHardDelete as u };
1076
+ export { resolveListLimitParam as a, assertReadableFields as c, decodeCursor as d, reconcileCursorOrder as f, parseQueryOptions as i, requestViewer as l, parseAggregateSelect as n, assertNoHiddenAggregateNames as o, parseGroupBy as r, assertQueryFieldsReadable as s, orderByEntriesToTuples as t, cursorToStartAfter as u };
632
1077
 
633
- //# sourceMappingURL=query-parser-DGRVFNM3.js.map
1078
+ //# sourceMappingURL=query-parser-DK9eMkb9.js.map