@sixb/core 0.1.14 → 0.1.16

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 (259) hide show
  1. package/dist/actions/index.js +26 -26
  2. package/dist/actions/worker.js +1 -1
  3. package/dist/agents/index.js +41 -41
  4. package/dist/agents/index.js.map +1 -1
  5. package/dist/agents/streams/index.js +2 -2
  6. package/dist/agents/streams/internal.js +2 -2
  7. package/dist/auth/index.js +6 -6
  8. package/dist/authorization/index.js +3 -3
  9. package/dist/bootstrap/index.js +44 -44
  10. package/dist/{chunk-6rzb1g6h.js → chunk-2xvgy3x5.js} +2 -2
  11. package/dist/{chunk-16jenmzw.js → chunk-3q7z9hd4.js} +33 -33
  12. package/dist/{chunk-16jenmzw.js.map → chunk-3q7z9hd4.js.map} +1 -1
  13. package/dist/{chunk-hxgj4kzd.js → chunk-40qb8wpe.js} +11 -11
  14. package/dist/{chunk-hxgj4kzd.js.map → chunk-40qb8wpe.js.map} +3 -3
  15. package/dist/{chunk-vn3yh259.js → chunk-4gmng3vf.js} +1 -1
  16. package/dist/{chunk-znw0zvy4.js → chunk-4wn7bxxa.js} +25 -7
  17. package/dist/chunk-4wn7bxxa.js.map +12 -0
  18. package/dist/{chunk-gct3m1da.js → chunk-667x4drs.js} +3 -3
  19. package/dist/{chunk-rzcjp5m2.js → chunk-69kt9q43.js} +2 -2
  20. package/dist/{chunk-3qpcrcj8.js → chunk-6aawea28.js} +2 -4
  21. package/dist/chunk-6aawea28.js.map +10 -0
  22. package/dist/{chunk-rzkr70gr.js → chunk-6fsr7y93.js} +5 -1
  23. package/dist/{chunk-rzkr70gr.js.map → chunk-6fsr7y93.js.map} +3 -3
  24. package/dist/{chunk-3fpgxpsq.js → chunk-6z1q7j19.js} +144 -73
  25. package/dist/{chunk-3fpgxpsq.js.map → chunk-6z1q7j19.js.map} +6 -6
  26. package/dist/{chunk-ypj2wqtj.js → chunk-70jmg2zx.js} +2 -2
  27. package/dist/{chunk-m8zkvep1.js → chunk-7g2gjvah.js} +1 -1
  28. package/dist/{chunk-pvgpckm1.js → chunk-8x74221e.js} +10 -10
  29. package/dist/{chunk-ma1mhh7w.js → chunk-a7ne02hk.js} +6 -6
  30. package/dist/{chunk-bxx3ms5t.js → chunk-b9y8hcm2.js} +2 -2
  31. package/dist/{chunk-ww185kt6.js → chunk-chdg6eqq.js} +2 -2
  32. package/dist/{chunk-y487ywrv.js → chunk-cq44v1a6.js} +4 -4
  33. package/dist/{chunk-4ypbe95r.js → chunk-cxj8y8t0.js} +2 -2
  34. package/dist/{chunk-3a338wrp.js → chunk-dp7yk20b.js} +1 -1
  35. package/dist/{chunk-dv4ct8mp.js → chunk-dsv8kdgk.js} +4 -4
  36. package/dist/{chunk-dsfw1bdr.js → chunk-f72rbvsb.js} +2 -2
  37. package/dist/{chunk-4tnrzqqh.js → chunk-fj1dvp37.js} +1 -1
  38. package/dist/{chunk-ht4a7mjg.js → chunk-g4rbm4a6.js} +42 -8
  39. package/dist/{chunk-ht4a7mjg.js.map → chunk-g4rbm4a6.js.map} +3 -3
  40. package/dist/{chunk-wnxabtn4.js → chunk-gkxsenvp.js} +19 -11
  41. package/dist/{chunk-wnxabtn4.js.map → chunk-gkxsenvp.js.map} +4 -4
  42. package/dist/{chunk-vef6qa14.js → chunk-gmdvns8f.js} +2 -2
  43. package/dist/{chunk-728fp090.js → chunk-h044jb97.js} +1072 -1075
  44. package/dist/chunk-h044jb97.js.map +15 -0
  45. package/dist/{chunk-00pfcqh4.js → chunk-h4138ww2.js} +130 -71
  46. package/dist/{chunk-00pfcqh4.js.map → chunk-h4138ww2.js.map} +5 -5
  47. package/dist/{chunk-yvf2jqj8.js → chunk-h866d223.js} +1 -1
  48. package/dist/{chunk-qma80ych.js → chunk-hn2r7z41.js} +3 -2
  49. package/dist/chunk-hn2r7z41.js.map +11 -0
  50. package/dist/{chunk-bdhfn0ww.js → chunk-kkqwzy6h.js} +1 -1
  51. package/dist/{chunk-wn7mznxc.js → chunk-kttr5ftm.js} +5 -5
  52. package/dist/{chunk-kxea54yg.js → chunk-kvysp909.js} +3 -3
  53. package/dist/{chunk-p1683c0v.js → chunk-kyjcwqz6.js} +5 -2
  54. package/dist/{chunk-p1683c0v.js.map → chunk-kyjcwqz6.js.map} +3 -3
  55. package/dist/{chunk-ffwvdgk1.js → chunk-nj0020ay.js} +1 -1
  56. package/dist/{chunk-q2xghyft.js → chunk-nkbev3tr.js} +5 -5
  57. package/dist/{chunk-cbn96bve.js → chunk-nvymebc1.js} +2 -2
  58. package/dist/{chunk-2fv8sajb.js → chunk-qnd7ehe5.js} +2 -2
  59. package/dist/{chunk-jfdggg1p.js → chunk-sbcm1b92.js} +5 -5
  60. package/dist/{chunk-d8yes9yx.js → chunk-swxzncrv.js} +1 -1
  61. package/dist/{chunk-drz59ve0.js → chunk-tkdbge6v.js} +1 -1
  62. package/dist/{chunk-ky7zpbtt.js → chunk-tnvafr7g.js} +1 -1
  63. package/dist/{chunk-y6vnvayj.js → chunk-vjcd8d27.js} +1 -1
  64. package/dist/{chunk-hj0n3c2p.js → chunk-vjqt7q3a.js} +45 -27
  65. package/dist/{chunk-hj0n3c2p.js.map → chunk-vjqt7q3a.js.map} +3 -3
  66. package/dist/{chunk-z9h698s8.js → chunk-vsvrv7n8.js} +1 -1
  67. package/dist/{chunk-xgptywp0.js → chunk-y2aq6ag9.js} +1 -1
  68. package/dist/{chunk-e2c5yxfe.js → chunk-zb47r1c8.js} +2 -2
  69. package/dist/{chunk-3nx8vj5a.js → chunk-zvmr2hqk.js} +3 -2
  70. package/dist/chunk-zvmr2hqk.js.map +12 -0
  71. package/dist/deploy/define.d.ts +4 -0
  72. package/dist/deploy/define.d.ts.map +1 -0
  73. package/dist/deploy/index.d.ts +3 -0
  74. package/dist/deploy/index.d.ts.map +1 -0
  75. package/dist/deploy/index.js +10 -0
  76. package/dist/deploy/index.js.map +10 -0
  77. package/dist/deploy/internal.d.ts +3 -0
  78. package/dist/deploy/internal.d.ts.map +1 -0
  79. package/dist/deploy/internal.js +244 -0
  80. package/dist/deploy/internal.js.map +11 -0
  81. package/dist/deploy/services.d.ts +8 -0
  82. package/dist/deploy/services.d.ts.map +1 -0
  83. package/dist/deploy/types.d.ts +309 -0
  84. package/dist/deploy/types.d.ts.map +1 -0
  85. package/dist/deploy/validation.d.ts +7 -0
  86. package/dist/deploy/validation.d.ts.map +1 -0
  87. package/dist/error-reporting/internal.js +3 -3
  88. package/dist/errors/catalog.d.ts +4 -0
  89. package/dist/errors/catalog.d.ts.map +1 -1
  90. package/dist/errors/internal.js +1 -1
  91. package/dist/events/index.js +3 -3
  92. package/dist/execution/agent.js +38 -38
  93. package/dist/execution/primitive.js +34 -34
  94. package/dist/execution/request.js +36 -36
  95. package/dist/execution/request.js.map +1 -1
  96. package/dist/index.js +56 -56
  97. package/dist/index.js.map +1 -1
  98. package/dist/maintenance/ontology-maintenance.d.ts +2 -0
  99. package/dist/maintenance/ontology-maintenance.d.ts.map +1 -1
  100. package/dist/maintenance/types.d.ts +8 -2
  101. package/dist/maintenance/types.d.ts.map +1 -1
  102. package/dist/materialization/dataset-watermark.d.ts +6 -1
  103. package/dist/materialization/dataset-watermark.d.ts.map +1 -1
  104. package/dist/materialization/index.js +1 -1
  105. package/dist/materialization/model.d.ts +6 -0
  106. package/dist/materialization/model.d.ts.map +1 -1
  107. package/dist/materializer/index.js +22 -22
  108. package/dist/materializer/index.js.map +1 -1
  109. package/dist/materializer/projections/finish-run.d.ts.map +1 -1
  110. package/dist/materializer/projections/replace.d.ts.map +1 -1
  111. package/dist/materializer/projections/source-materialization.d.ts +6 -2
  112. package/dist/materializer/projections/source-materialization.d.ts.map +1 -1
  113. package/dist/models/embedding-model.d.ts +4 -0
  114. package/dist/models/embedding-model.d.ts.map +1 -1
  115. package/dist/models/execution/index.js +19 -19
  116. package/dist/models/index.js +2 -2
  117. package/dist/objects/index.js +25 -25
  118. package/dist/objects/query/index.js +21 -21
  119. package/dist/objects/query/index.js.map +1 -1
  120. package/dist/objects/vectors/indexing-generate.d.ts.map +1 -1
  121. package/dist/objects/vectors/profile.d.ts.map +1 -1
  122. package/dist/objects/vectors/query.d.ts +1 -1
  123. package/dist/objects/vectors/query.d.ts.map +1 -1
  124. package/dist/objects/vectors/resolve-query.d.ts.map +1 -1
  125. package/dist/objects/vectors/types.d.ts +1 -1
  126. package/dist/objects/vectors/types.d.ts.map +1 -1
  127. package/dist/ontology/builders.d.ts +13 -4
  128. package/dist/ontology/builders.d.ts.map +1 -1
  129. package/dist/ontology/index.js +3 -3
  130. package/dist/ontology/internal.js +2 -2
  131. package/dist/ontology/vector-profiles.d.ts.map +1 -1
  132. package/dist/pipelines/index.js +23 -23
  133. package/dist/projections/internal.js +20 -20
  134. package/dist/projections/types.d.ts +1 -1
  135. package/dist/projections/types.d.ts.map +1 -1
  136. package/dist/queues/in-memory.d.ts +1 -1
  137. package/dist/queues/in-memory.d.ts.map +1 -1
  138. package/dist/runtime/internal.js +23 -23
  139. package/dist/sandboxes/index.js +5 -5
  140. package/dist/shares/internal.js +18 -18
  141. package/dist/shares/internal.js.map +1 -1
  142. package/dist/storage/action-runs/failure.js +2 -2
  143. package/dist/storage/agents/provider.js +1 -1
  144. package/dist/storage/ai-limits/enforcement.js +2 -2
  145. package/dist/storage/connector-connections/provider.js +1 -1
  146. package/dist/storage/file-upload-sessions/provider.js +7 -7
  147. package/dist/storage/index.d.ts +2 -2
  148. package/dist/storage/index.d.ts.map +1 -1
  149. package/dist/storage/index.js +33 -33
  150. package/dist/storage/index.js.map +1 -1
  151. package/dist/storage/internal.js +1 -1
  152. package/dist/storage/ontology/in-memory/sources.d.ts +6 -3
  153. package/dist/storage/ontology/in-memory/sources.d.ts.map +1 -1
  154. package/dist/storage/ontology/index.d.ts +1 -1
  155. package/dist/storage/ontology/index.d.ts.map +1 -1
  156. package/dist/storage/ontology/provider.d.ts +16 -3
  157. package/dist/storage/ontology/provider.d.ts.map +1 -1
  158. package/dist/storage/ontology/provider.js +8 -4
  159. package/dist/storage/ontology/provider.js.map +1 -1
  160. package/dist/storage/ontology/sources.d.ts +36 -8
  161. package/dist/storage/ontology/sources.d.ts.map +1 -1
  162. package/dist/storage/projection-runs/in-memory.d.ts +3 -1
  163. package/dist/storage/projection-runs/in-memory.d.ts.map +1 -1
  164. package/dist/storage/projection-runs/index.d.ts +1 -1
  165. package/dist/storage/projection-runs/index.d.ts.map +1 -1
  166. package/dist/storage/projection-runs/lifecycle.d.ts +12 -0
  167. package/dist/storage/projection-runs/lifecycle.d.ts.map +1 -1
  168. package/dist/storage/projection-runs/provider.js +10 -4
  169. package/dist/storage/projection-runs/provider.js.map +1 -1
  170. package/dist/storage/projection-runs/types.d.ts +32 -1
  171. package/dist/storage/projection-runs/types.d.ts.map +1 -1
  172. package/dist/storage/workflow-runs/provider.js +7 -7
  173. package/dist/syncs/index.js +24 -24
  174. package/dist/testing/index.js +306 -64
  175. package/dist/testing/index.js.map +4 -4
  176. package/dist/testing/ontology-storage-contract.d.ts.map +1 -1
  177. package/dist/testing/projection-run-storage-contract.d.ts.map +1 -1
  178. package/dist/webhooks/internal.js +19 -19
  179. package/dist/webhooks/internal.js.map +1 -1
  180. package/dist/workers/index.js +1 -1
  181. package/dist/workflows/index.js +41 -41
  182. package/package.json +13 -1
  183. package/src/deploy/define.ts +6 -0
  184. package/src/deploy/index.ts +41 -0
  185. package/src/deploy/internal.ts +7 -0
  186. package/src/deploy/services.ts +51 -0
  187. package/src/deploy/types.ts +338 -0
  188. package/src/deploy/validation.ts +261 -0
  189. package/src/errors/catalog.ts +4 -0
  190. package/src/maintenance/ontology-maintenance.ts +32 -2
  191. package/src/maintenance/types.ts +8 -2
  192. package/src/materialization/dataset-watermark.ts +6 -4
  193. package/src/materialization/model.ts +7 -0
  194. package/src/materializer/README.md +26 -6
  195. package/src/materializer/projections/finish-run.ts +129 -26
  196. package/src/materializer/projections/replace.ts +67 -49
  197. package/src/materializer/projections/source-materialization.ts +11 -6
  198. package/src/models/embedding-model.ts +12 -0
  199. package/src/models/execution/embedding.ts +5 -1
  200. package/src/objects/query/validate.ts +1 -10
  201. package/src/objects/vectors/README.md +8 -0
  202. package/src/objects/vectors/handle.ts +1 -1
  203. package/src/objects/vectors/indexing-generate.ts +5 -1
  204. package/src/objects/vectors/profile.ts +2 -0
  205. package/src/objects/vectors/query.ts +18 -4
  206. package/src/objects/vectors/resolve-query.ts +9 -5
  207. package/src/objects/vectors/types.ts +3 -1
  208. package/src/ontology/builders.ts +21 -7
  209. package/src/ontology/vector-profiles.ts +1 -0
  210. package/src/projections/types.ts +1 -0
  211. package/src/storage/index.ts +6 -1
  212. package/src/storage/ontology/in-memory/sources.ts +119 -55
  213. package/src/storage/ontology/index.ts +4 -1
  214. package/src/storage/ontology/provider.ts +64 -7
  215. package/src/storage/ontology/sources.ts +42 -8
  216. package/src/storage/projection-runs/in-memory.ts +41 -2
  217. package/src/storage/projection-runs/index.ts +2 -0
  218. package/src/storage/projection-runs/lifecycle.ts +61 -4
  219. package/src/storage/projection-runs/types.ts +41 -1
  220. package/src/testing/ontology-storage-contract.ts +141 -15
  221. package/src/testing/projection-run-storage-contract.ts +174 -1
  222. package/dist/chunk-3nx8vj5a.js.map +0 -12
  223. package/dist/chunk-3qpcrcj8.js.map +0 -10
  224. package/dist/chunk-728fp090.js.map +0 -15
  225. package/dist/chunk-qma80ych.js.map +0 -11
  226. package/dist/chunk-znw0zvy4.js.map +0 -12
  227. /package/dist/{chunk-6rzb1g6h.js.map → chunk-2xvgy3x5.js.map} +0 -0
  228. /package/dist/{chunk-vn3yh259.js.map → chunk-4gmng3vf.js.map} +0 -0
  229. /package/dist/{chunk-gct3m1da.js.map → chunk-667x4drs.js.map} +0 -0
  230. /package/dist/{chunk-rzcjp5m2.js.map → chunk-69kt9q43.js.map} +0 -0
  231. /package/dist/{chunk-ypj2wqtj.js.map → chunk-70jmg2zx.js.map} +0 -0
  232. /package/dist/{chunk-m8zkvep1.js.map → chunk-7g2gjvah.js.map} +0 -0
  233. /package/dist/{chunk-pvgpckm1.js.map → chunk-8x74221e.js.map} +0 -0
  234. /package/dist/{chunk-ma1mhh7w.js.map → chunk-a7ne02hk.js.map} +0 -0
  235. /package/dist/{chunk-bxx3ms5t.js.map → chunk-b9y8hcm2.js.map} +0 -0
  236. /package/dist/{chunk-ww185kt6.js.map → chunk-chdg6eqq.js.map} +0 -0
  237. /package/dist/{chunk-y487ywrv.js.map → chunk-cq44v1a6.js.map} +0 -0
  238. /package/dist/{chunk-4ypbe95r.js.map → chunk-cxj8y8t0.js.map} +0 -0
  239. /package/dist/{chunk-3a338wrp.js.map → chunk-dp7yk20b.js.map} +0 -0
  240. /package/dist/{chunk-dv4ct8mp.js.map → chunk-dsv8kdgk.js.map} +0 -0
  241. /package/dist/{chunk-dsfw1bdr.js.map → chunk-f72rbvsb.js.map} +0 -0
  242. /package/dist/{chunk-4tnrzqqh.js.map → chunk-fj1dvp37.js.map} +0 -0
  243. /package/dist/{chunk-vef6qa14.js.map → chunk-gmdvns8f.js.map} +0 -0
  244. /package/dist/{chunk-yvf2jqj8.js.map → chunk-h866d223.js.map} +0 -0
  245. /package/dist/{chunk-bdhfn0ww.js.map → chunk-kkqwzy6h.js.map} +0 -0
  246. /package/dist/{chunk-wn7mznxc.js.map → chunk-kttr5ftm.js.map} +0 -0
  247. /package/dist/{chunk-kxea54yg.js.map → chunk-kvysp909.js.map} +0 -0
  248. /package/dist/{chunk-ffwvdgk1.js.map → chunk-nj0020ay.js.map} +0 -0
  249. /package/dist/{chunk-q2xghyft.js.map → chunk-nkbev3tr.js.map} +0 -0
  250. /package/dist/{chunk-cbn96bve.js.map → chunk-nvymebc1.js.map} +0 -0
  251. /package/dist/{chunk-2fv8sajb.js.map → chunk-qnd7ehe5.js.map} +0 -0
  252. /package/dist/{chunk-jfdggg1p.js.map → chunk-sbcm1b92.js.map} +0 -0
  253. /package/dist/{chunk-d8yes9yx.js.map → chunk-swxzncrv.js.map} +0 -0
  254. /package/dist/{chunk-drz59ve0.js.map → chunk-tkdbge6v.js.map} +0 -0
  255. /package/dist/{chunk-ky7zpbtt.js.map → chunk-tnvafr7g.js.map} +0 -0
  256. /package/dist/{chunk-y6vnvayj.js.map → chunk-vjcd8d27.js.map} +0 -0
  257. /package/dist/{chunk-z9h698s8.js.map → chunk-vsvrv7n8.js.map} +0 -0
  258. /package/dist/{chunk-xgptywp0.js.map → chunk-y2aq6ag9.js.map} +0 -0
  259. /package/dist/{chunk-e2c5yxfe.js.map → chunk-zb47r1c8.js.map} +0 -0
@@ -3,7 +3,7 @@ import {
3
3
  MAX_OBJECT_READ_FACETS2,
4
4
  isAllowed2,
5
5
  assertAuthorized2
6
- } from "./chunk-rzcjp5m2.js";
6
+ } from "./chunk-69kt9q43.js";
7
7
  import {
8
8
  validateSchemaValue2,
9
9
  validatePropertyValue2,
@@ -12,7 +12,7 @@ import {
12
12
  isTextSchema,
13
13
  isContainsSchema,
14
14
  queryScalarKindForSchema
15
- } from "./chunk-d8yes9yx.js";
15
+ } from "./chunk-swxzncrv.js";
16
16
  import {
17
17
  hasVectorProfile2,
18
18
  isVectorProfileQuery2,
@@ -24,7 +24,7 @@ import {
24
24
  MAX_VECTOR_K,
25
25
  vectorConfiguration,
26
26
  normalizeVector2
27
- } from "./chunk-3fpgxpsq.js";
27
+ } from "./chunk-6z1q7j19.js";
28
28
  import {
29
29
  normalizeObjectQuery2,
30
30
  ObjectQueryValidationError2,
@@ -33,7 +33,7 @@ import {
33
33
  } from "./chunk-a0073121.js";
34
34
  import {
35
35
  sameEmbeddingModel
36
- } from "./chunk-p1683c0v.js";
36
+ } from "./chunk-kyjcwqz6.js";
37
37
  import {
38
38
  formatUnknownObjectTypeMessage
39
39
  } from "./chunk-cqr3acm1.js";
@@ -43,1214 +43,1211 @@ import {
43
43
  import {
44
44
  stableJsonStringify2
45
45
  } from "./chunk-wy0qdqfm.js";
46
- // src/objects/vectors/resolve-query.ts
47
- var QUERY_EMBEDDING_TIMEOUT_MS = 30000;
48
- async function resolveVectorSearchText(query, ontology, models, signal) {
49
- if (query.kind === "limit" || query.kind === "project") {
50
- const input = await resolveVectorSearchText(query.input, ontology, models, signal);
51
- return { ...query, input };
52
- }
53
- if (query.kind !== "vector" || typeof query.vector !== "string")
54
- return query;
55
- const model = resolveProfileEmbeddingModel(query, ontology, models);
56
- const vector = await embedSearchText(query.vector, model, signal);
57
- return { ...query, vector };
58
- }
59
- function resolveProfileEmbeddingModel(query, ontology, models) {
60
- let input = query.input;
61
- while (input.kind === "filter")
62
- input = input.input;
63
- if (input.kind !== "start" || !query.profile) {
64
- throw new ObjectQueryExecutionError2("embedding_model_unavailable", "Vector search requires the embedding model registered for its profile.");
65
- }
66
- const profile = ontology.resolveObjectType(input.objectTypeId).search?.vectors?.[query.profile];
67
- if (!profile) {
68
- throw new ObjectQueryExecutionError2("embedding_model_unavailable", "Vector search requires the embedding model registered for its profile.");
69
- }
70
- const model = models?.getByRef({
71
- provider: profile.model.providerId,
72
- modelId: profile.model.modelId
73
- })?.model;
74
- if (!model || !sameEmbeddingModel(model, profile.model)) {
75
- throw new ObjectQueryExecutionError2("embedding_model_unavailable", "Vector search requires the embedding model registered for its profile.");
76
- }
77
- return model;
78
- }
79
- async function embedSearchText(text, model, signal) {
80
- const dimensions = model.definition.dimensions;
81
- const timeout = AbortSignal.timeout(QUERY_EMBEDDING_TIMEOUT_MS);
82
- const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
83
- requestSignal.throwIfAborted();
84
- const result = await model.embed({ texts: [text], signal: requestSignal });
85
- requestSignal.throwIfAborted();
86
- if (!Array.isArray(result?.vectors) || result.vectors.length !== 1) {
87
- throw new ObjectQueryExecutionError2("invalid_embedding_response", "Embedding model must return exactly one vector for the search text.");
88
- }
89
- try {
90
- return normalizeVector2(result.vectors[0], dimensions);
91
- } catch {
92
- throw new ObjectQueryExecutionError2("invalid_embedding_response", "Embedding model returned an invalid vector for the search profile.");
93
- }
46
+ // src/objects/query/validate.ts
47
+ var DEFAULT_MAX_LIMIT = 1000;
48
+ var DEFAULT_MAX_PAGE_SIZE = 1000;
49
+ var DEFAULT_MAX_REFS = 1000;
50
+ var NOOP_ADMISSION_STATE = Object.freeze({});
51
+ var NOOP_QUERY_ADMISSION = Object.freeze({
52
+ empty: () => NOOP_ADMISSION_STATE,
53
+ source: () => ({ state: NOOP_ADMISSION_STATE }),
54
+ property: () => {
55
+ return;
56
+ },
57
+ edge: (input) => ({
58
+ state: input.state
59
+ }),
60
+ set: (input) => input.states[0] ?? NOOP_ADMISSION_STATE
61
+ });
62
+ function validateObjectQuery2(query, options) {
63
+ const admitted = validateObjectQueryWithAdmission(query, options, NOOP_QUERY_ADMISSION);
64
+ return {
65
+ query: admitted.query,
66
+ result: admitted.result,
67
+ touchedObjectTypeIds: admitted.touchedObjectTypeIds
68
+ };
94
69
  }
95
-
96
- // src/objects/query/planner.ts
97
- var DEFAULT_MAX_FALLBACK_ROWS = 1000;
98
- var fallbackNodeKinds = new Set([
99
- "start",
100
- "refs",
101
- "filter",
102
- "sort",
103
- "limit",
104
- "page",
105
- "project",
106
- "expand"
107
- ]);
108
- function planObjectQuery2(query, options) {
109
- const operation = options.operation ?? "queryObjects";
110
- const providerIssues = collectProviderIssues(query, options.capabilities, {
111
- operation,
112
- hasImplementation: hasProviderImplementation(operation, options)
113
- });
114
- if (providerIssues.length === 0) {
115
- return {
116
- mode: "pushdown",
117
- query,
118
- providerIssues,
119
- fallbackIssues: [],
120
- issues: []
121
- };
122
- }
123
- const fallbackIssues = collectFallbackIssues(query, options);
124
- const issues = [...providerIssues, ...fallbackIssues];
125
- if (fallbackIssues.length === 0) {
126
- return {
127
- mode: "fallback",
128
- query,
129
- providerIssues,
130
- fallbackIssues,
131
- issues: [],
132
- fallback: {
133
- maxRows: options.maxFallbackRows ?? DEFAULT_MAX_FALLBACK_ROWS,
134
- requiresExplicitBound: options.requiresExplicitFallbackBound !== false
135
- }
136
- };
70
+ function validateObjectQueryWithAdmission(query, options, admission) {
71
+ const normalized = options.normalize === false ? query : normalizeObjectQuery2(query);
72
+ const ctx = createValidationContext(options, admission);
73
+ validateVectorComposition(query, ctx);
74
+ const validation = validateQueryNode(normalized, "$", ctx);
75
+ if (ctx.issues.length > 0) {
76
+ throw new ObjectQueryValidationError2(ctx.issues);
137
77
  }
78
+ const denial = ctx.admissionDenials[0];
79
+ if (denial)
80
+ throw denial;
138
81
  return {
139
- mode: "rejected",
140
- query,
141
- providerIssues,
142
- fallbackIssues,
143
- issues
82
+ query: validation.query,
83
+ result: validation.result,
84
+ touchedObjectTypeIds: [...ctx.touchedObjectTypeIds],
85
+ admissionState: validation.admissionState
144
86
  };
145
87
  }
146
- function collectProviderIssues(query, capabilities, options) {
147
- const issues = [];
148
- collectOperationProviderIssues(capabilities, options, issues);
149
- collectNodeProviderIssues(query, "$", capabilities, issues);
150
- return issues;
88
+ function collectObjectQueryValidationIssues2(query, options) {
89
+ const normalized = options.normalize === false ? query : normalizeObjectQuery2(query);
90
+ const ctx = createValidationContext(options, NOOP_QUERY_ADMISSION);
91
+ validateVectorComposition(query, ctx);
92
+ validateQueryNode(normalized, "$", ctx);
93
+ return ctx.issues;
151
94
  }
152
- function collectOperationProviderIssues(capabilities, options, issues) {
153
- const enabled = options.operation === "queryObjects" ? capabilities.queryObjects === true : options.operation === "countObjects" ? capabilities.countObjects === true : options.operation === "existsObjects" ? capabilities.existsObjects === true : capabilities.facetObjects === true;
154
- if (!enabled) {
155
- const code = `${operationIssuePrefix(options.operation)}_not_enabled`;
156
- addIssue(issues, "$", code, `Object storage capabilities do not enable ${options.operation} pushdown`);
157
- }
158
- if (options.hasImplementation)
159
- return;
160
- const code = `${operationIssuePrefix(options.operation)}_not_implemented`;
161
- addIssue(issues, "$", code, `Object storage does not implement ${options.operation}`);
95
+ function resolveObjectQueryResultShape2(query, options) {
96
+ return validateObjectQuery2(query, options).result;
162
97
  }
163
- function hasProviderImplementation(operation, options) {
164
- switch (operation) {
165
- case "queryObjects":
166
- return options.hasQueryObjects === true;
167
- case "countObjects":
168
- return options.hasCountObjects === true;
169
- case "existsObjects":
170
- return options.hasExistsObjects === true;
171
- case "facetObjects":
172
- return options.hasFacetObjects === true;
173
- }
98
+ function validateVectorComposition(query, ctx) {
99
+ if (hasVectorProfile2(query) && !isVectorProfileQuery2(query))
100
+ ctx.issues.push({
101
+ path: "$",
102
+ code: "vector.composition",
103
+ message: "Vector profiles support one top-k over one concrete type, with filters, traversals or sets before ranking and optional limit/project after it."
104
+ });
174
105
  }
175
- function operationIssuePrefix(operation) {
176
- switch (operation) {
177
- case "queryObjects":
178
- return "query_objects";
179
- case "countObjects":
180
- return "count_objects";
181
- case "existsObjects":
182
- return "exists_objects";
183
- case "facetObjects":
184
- return "facet_objects";
185
- }
106
+ function createValidationContext(options, admission) {
107
+ return {
108
+ ontology: options.ontology,
109
+ valueTypesById: options.ontology.getValueTypesById(),
110
+ maxLimit: options.maxLimit ?? DEFAULT_MAX_LIMIT,
111
+ maxPageSize: options.maxPageSize ?? DEFAULT_MAX_PAGE_SIZE,
112
+ maxRefs: options.maxRefs ?? DEFAULT_MAX_REFS,
113
+ issues: [],
114
+ touchedObjectTypeIds: new Set,
115
+ admission,
116
+ admissionDenials: []
117
+ };
186
118
  }
187
- function collectNodeProviderIssues(query, path, capabilities, issues) {
188
- if (capabilities.nodes?.[query.kind] !== true) {
189
- addIssue(issues, path, "query_node_not_supported", `Provider does not support query node '${query.kind}'`);
119
+ function validateQueryNode(query, path, ctx) {
120
+ const validation = dispatchQueryNode(query, path, ctx);
121
+ for (const objectTypeId of validation.result.objectTypeIds) {
122
+ ctx.touchedObjectTypeIds.add(objectTypeId);
190
123
  }
124
+ return validation;
125
+ }
126
+ function dispatchQueryNode(query, path, ctx) {
191
127
  switch (query.kind) {
192
- case "start":
193
- if (query.includeSubtypes === true && capabilities.features?.includeSubtypes !== true) {
194
- addIssue(issues, path, "include_subtypes_not_supported", "Provider does not support start.includeSubtypes expansion");
195
- }
196
- return;
197
- case "refs":
198
- return;
199
- case "filter":
200
- collectPredicateProviderIssues(query.predicate, `${path}.predicate`, capabilities, issues);
201
- collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
202
- return;
203
- case "text":
204
- case "vector":
205
- case "project":
206
- collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
207
- return;
208
- case "limit":
209
- collectProviderMaxLimitIssues(query.limit, path, capabilities, issues);
210
- collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
211
- return;
212
- case "page":
213
- collectProviderMaxPageSizeIssues(query.pageSize, path, capabilities, issues);
214
- collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
215
- return;
216
- case "traverse":
217
- if (capabilities.traversalDirections?.[query.direction] !== true) {
218
- addIssue(issues, path, "traversal_direction_not_supported", `Provider does not support ${query.direction} traversal`);
219
- }
220
- collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
221
- return;
128
+ case "start": {
129
+ const validation = validateStart(query.objectTypeId, query.includeSubtypes, path, ctx);
130
+ return {
131
+ ...validation,
132
+ admissionState: admitTransition(ctx, ctx.admission.source({
133
+ kind: "start",
134
+ result: validation.result,
135
+ objectTypeId: query.objectTypeId,
136
+ path
137
+ }))
138
+ };
139
+ }
140
+ case "refs": {
141
+ const validation = validateRefs(query.refs, path, ctx);
142
+ return {
143
+ ...validation,
144
+ admissionState: admitTransition(ctx, ctx.admission.source({
145
+ kind: "refs",
146
+ result: validation.result,
147
+ refs: validation.query.kind === "refs" ? validation.query.refs : query.refs,
148
+ path
149
+ }))
150
+ };
151
+ }
152
+ case "filter": {
153
+ const input = validateQueryNode(query.input, `${path}.input`, ctx);
154
+ const predicate = validatePredicate(query.predicate, input.result, input.admissionState, `${path}.predicate`, ctx);
155
+ return {
156
+ result: input.result,
157
+ query: { ...query, input: input.query, predicate },
158
+ admissionState: input.admissionState
159
+ };
160
+ }
161
+ case "text": {
162
+ const input = validateQueryNode(query.input, `${path}.input`, ctx);
163
+ const textFields = validateTextQuery(query.query, query.fields, input.result, input.admissionState, path, ctx);
164
+ return {
165
+ result: input.result,
166
+ query: {
167
+ ...query,
168
+ input: input.query,
169
+ fields: query.fields,
170
+ fieldsByObjectType: query.fields ? undefined : textFields.fieldsByObjectType
171
+ },
172
+ admissionState: input.admissionState
173
+ };
174
+ }
175
+ case "vector": {
176
+ const input = validateQueryNode(query.input, `${path}.input`, ctx);
177
+ return {
178
+ result: input.result,
179
+ query: validateVectorProfileQuery({ ...query, input: input.query }, input.result, input.admissionState, path, ctx),
180
+ admissionState: input.admissionState
181
+ };
182
+ }
183
+ case "traverse": {
184
+ const input = validateQueryNode(query.input, `${path}.input`, ctx);
185
+ const result = validateTraverse(query.linkId, query.direction, query.sourceObjectTypeId, input.result, path, ctx);
186
+ return {
187
+ result,
188
+ query: { ...query, input: input.query },
189
+ admissionState: admitTransition(ctx, ctx.admission.edge({
190
+ state: input.admissionState,
191
+ result,
192
+ linkId: query.linkId,
193
+ direction: query.direction,
194
+ sourceObjectTypeId: query.sourceObjectTypeId,
195
+ use: "traverse",
196
+ path
197
+ }))
198
+ };
199
+ }
222
200
  case "set":
223
- if (capabilities.setOps?.[query.op] !== true) {
224
- addIssue(issues, path, "set_operation_not_supported", `Provider does not support set operation '${query.op}'`);
225
- }
226
- query.inputs.forEach((input, index) => {
227
- collectNodeProviderIssues(input, `${path}.inputs[${index}]`, capabilities, issues);
228
- });
229
- return;
230
- case "sort":
231
- query.fields.forEach((field, index) => {
232
- collectSortProviderIssues(field, `${path}.fields[${index}]`, capabilities, issues);
233
- });
234
- collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
235
- return;
236
- case "expand":
237
- query.expansions.forEach((expansion, index) => {
238
- collectExpansionProviderIssues(expansion, `${path}.expansions[${index}]`, capabilities, issues);
239
- });
240
- collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
241
- return;
201
+ return validateSet(query.inputs, query.op, path, ctx);
202
+ case "sort": {
203
+ const input = validateQueryNode(query.input, `${path}.input`, ctx);
204
+ const fields = validateSortFields(query.fields, input.result, input.admissionState, "sort", path, ctx);
205
+ return {
206
+ result: input.result,
207
+ query: { ...query, input: input.query, fields },
208
+ admissionState: input.admissionState
209
+ };
210
+ }
211
+ case "limit": {
212
+ const input = validateQueryNode(query.input, `${path}.input`, ctx);
213
+ validateLimit(query.limit, path, ctx);
214
+ return {
215
+ result: input.result,
216
+ query: { ...query, input: input.query },
217
+ admissionState: input.admissionState
218
+ };
219
+ }
220
+ case "page": {
221
+ const input = validateQueryNode(query.input, `${path}.input`, ctx);
222
+ validatePage(query.pageSize, query.pageToken, path, ctx);
223
+ return {
224
+ result: input.result,
225
+ query: { ...query, input: input.query },
226
+ admissionState: input.admissionState
227
+ };
228
+ }
229
+ case "project": {
230
+ const input = validateQueryNode(query.input, `${path}.input`, ctx);
231
+ validateProjection(query.properties, input.result, input.admissionState, path, ctx);
232
+ return {
233
+ result: input.result,
234
+ query: { ...query, input: input.query },
235
+ admissionState: input.admissionState
236
+ };
237
+ }
238
+ case "expand": {
239
+ const input = validateQueryNode(query.input, `${path}.input`, ctx);
240
+ const expansions = validateExpansions(query.expansions, input.result, input.admissionState, `${path}.expansions`, ctx);
241
+ return {
242
+ result: input.result,
243
+ query: { ...query, input: input.query, expansions },
244
+ admissionState: input.admissionState
245
+ };
246
+ }
242
247
  }
243
248
  }
244
- function collectExpansionProviderIssues(expansion, path, capabilities, issues) {
245
- if (expansion.cardinality === undefined) {
246
- addIssue(issues, path, "expand_cardinality_unresolved", `Provider cannot push down expansion '${expansion.linkId}' without a resolved cardinality`);
249
+ function validateRefs(refs, path, ctx) {
250
+ if (refs.length === 0) {
251
+ addIssue(ctx, `${path}.refs`, "empty_refs", "refs must include at least one object reference");
247
252
  }
248
- expansion.orderBy?.forEach((field, index) => {
249
- collectSortProviderIssues(field, `${path}.orderBy[${index}]`, capabilities, issues);
250
- });
251
- expansion.expand?.forEach((nested, index) => {
252
- collectExpansionProviderIssues(nested, `${path}.expand[${index}]`, capabilities, issues);
253
+ if (refs.length > ctx.maxRefs) {
254
+ addIssue(ctx, `${path}.refs`, "too_many_refs", `refs must include at most ${ctx.maxRefs} object references`);
255
+ }
256
+ const objectTypeIds = new Set;
257
+ refs.forEach((ref, index) => {
258
+ const refPath = `${path}.refs[${index}]`;
259
+ if (!ref.objectTypeId) {
260
+ addIssue(ctx, `${refPath}.objectTypeId`, "missing_object_type", "objectTypeId is required");
261
+ } else if (!ctx.ontology.getObjectTypeById(ref.objectTypeId)) {
262
+ addIssue(ctx, `${refPath}.objectTypeId`, "unknown_object_type", formatUnknownObjectTypeMessage(ref.objectTypeId));
263
+ } else {
264
+ objectTypeIds.add(ref.objectTypeId);
265
+ }
266
+ if (!ref.primaryId) {
267
+ addIssue(ctx, `${refPath}.primaryId`, "missing_primary_id", "primaryId is required");
268
+ }
253
269
  });
270
+ return {
271
+ result: { objectTypeIds: [...objectTypeIds] },
272
+ query: { kind: "refs", refs: refs.map((ref) => ({ ...ref })) }
273
+ };
254
274
  }
255
- function collectPredicateProviderIssues(predicate, path, capabilities, issues) {
256
- const scalarOperation = predicateScalarOperation(predicate);
257
- if ("scalarKind" in predicate && predicate.scalarKind !== undefined && scalarOperation !== undefined && capabilities.scalarOperations?.[predicate.scalarKind]?.[scalarOperation] !== true) {
258
- addIssue(issues, path, "scalar_operation_not_supported", `Provider does not support ${scalarOperation} for '${predicate.scalarKind}' values`);
275
+ function validateStart(objectTypeId, includeSubtypes, path, ctx) {
276
+ if (!objectTypeId) {
277
+ addIssue(ctx, path, "missing_object_type", "start.objectTypeId is required");
278
+ return { result: { objectTypeIds: [] }, query: { kind: "start", objectTypeId } };
259
279
  }
260
- if (capabilities.predicateOps?.[predicate.op] !== true) {
261
- addIssue(issues, path, "predicate_operation_not_supported", `Provider does not support predicate operation '${predicate.op}'`);
280
+ if (!ctx.ontology.getObjectTypeById(objectTypeId)) {
281
+ addIssue(ctx, path, "unknown_object_type", formatUnknownObjectTypeMessage(objectTypeId));
282
+ return { result: { objectTypeIds: [] }, query: { kind: "start", objectTypeId } };
262
283
  }
284
+ const objectTypeIds = includeSubtypes ? uniqueStrings([objectTypeId, ...ctx.ontology.listSubTypes(objectTypeId)]) : [objectTypeId];
285
+ return { result: { objectTypeIds }, query: { kind: "start", objectTypeId, includeSubtypes } };
286
+ }
287
+ function validatePredicate(predicate, shape, admissionState, path, ctx) {
263
288
  switch (predicate.op) {
264
289
  case "and":
265
290
  case "or":
266
- predicate.items.forEach((item, index) => {
267
- collectPredicateProviderIssues(item, `${path}.items[${index}]`, capabilities, issues);
268
- });
269
- return;
291
+ if (predicate.items.length === 0) {
292
+ addIssue(ctx, path, "empty_predicate_group", `Predicate '${predicate.op}' must not be empty`);
293
+ }
294
+ return {
295
+ ...predicate,
296
+ items: predicate.items.map((item, index) => validatePredicate(item, shape, admissionState, `${path}.items[${index}]`, ctx))
297
+ };
270
298
  case "not":
271
- collectPredicateProviderIssues(predicate.item, `${path}.item`, capabilities, issues);
272
- return;
273
- default:
274
- return;
275
- }
276
- }
277
- function predicateScalarOperation(predicate) {
278
- switch (predicate.op) {
299
+ return {
300
+ ...predicate,
301
+ item: validatePredicate(predicate.item, shape, admissionState, `${path}.item`, ctx)
302
+ };
279
303
  case "eq":
280
304
  case "neq":
281
- case "in":
282
- case "contains":
283
- return "equality";
284
305
  case "lt":
285
306
  case "lte":
286
307
  case "gt":
287
- case "gte":
288
- return "ordering";
289
- default:
290
- return;
291
- }
292
- }
293
- function collectSortProviderIssues(field, path, capabilities, issues) {
294
- if (field.kind === "property" && field.scalarKind !== undefined && capabilities.scalarOperations?.[field.scalarKind]?.ordering !== true) {
295
- addIssue(issues, path, "scalar_operation_not_supported", `Provider does not support ordering for '${field.scalarKind}' values`);
308
+ case "gte": {
309
+ const { scalarKind: _authoredScalarKind, ...authoredPredicate } = predicate;
310
+ const scalarKind = resolveAndValidatePredicateProperty(predicate.propertyId, predicate.op, shape, path, ctx);
311
+ validatePredicateValue(predicate.propertyId, predicate.value, shape, path, ctx);
312
+ admitProperty(ctx, {
313
+ state: admissionState,
314
+ propertyId: predicate.propertyId,
315
+ use: "filter",
316
+ path
317
+ });
318
+ return {
319
+ ...authoredPredicate,
320
+ value: normalizeObjectQueryValue(predicate.value, scalarKind),
321
+ ...scalarKind ? { scalarKind } : {}
322
+ };
323
+ }
324
+ case "in": {
325
+ const { scalarKind: _authoredScalarKind, ...authoredPredicate } = predicate;
326
+ const scalarKind = resolveAndValidatePredicateProperty(predicate.propertyId, predicate.op, shape, path, ctx);
327
+ if (predicate.values.length === 0) {
328
+ addIssue(ctx, path, "empty_in_values", "Predicate 'in' must include at least one value");
329
+ }
330
+ predicate.values.forEach((value, index) => {
331
+ validatePredicateValue(predicate.propertyId, value, shape, `${path}.values[${index}]`, ctx);
332
+ });
333
+ admitProperty(ctx, {
334
+ state: admissionState,
335
+ propertyId: predicate.propertyId,
336
+ use: "filter",
337
+ path
338
+ });
339
+ return {
340
+ ...authoredPredicate,
341
+ values: predicate.values.map((value) => normalizeObjectQueryValue(value, scalarKind)),
342
+ ...scalarKind ? { scalarKind } : {}
343
+ };
344
+ }
345
+ case "exists":
346
+ resolveAndValidatePredicateProperty(predicate.propertyId, predicate.op, shape, path, ctx);
347
+ admitProperty(ctx, {
348
+ state: admissionState,
349
+ propertyId: predicate.propertyId,
350
+ use: "filter",
351
+ path
352
+ });
353
+ if (typeof predicate.value !== "boolean") {
354
+ addIssue(ctx, path, "invalid_exists_value", "Predicate 'exists' value must be boolean");
355
+ }
356
+ return predicate;
357
+ case "contains": {
358
+ const { scalarKind: _authoredScalarKind, ...authoredPredicate } = predicate;
359
+ const scalarKind = resolveAndValidatePredicateProperty(predicate.propertyId, predicate.op, shape, path, ctx);
360
+ validateContainsValue(predicate.propertyId, predicate.value, shape, path, ctx);
361
+ admitProperty(ctx, {
362
+ state: admissionState,
363
+ propertyId: predicate.propertyId,
364
+ use: "filter",
365
+ path
366
+ });
367
+ return {
368
+ ...authoredPredicate,
369
+ value: normalizeObjectQueryValue(predicate.value, scalarKind),
370
+ ...scalarKind ? { scalarKind } : {}
371
+ };
372
+ }
296
373
  }
297
- if (capabilities.sortKinds?.[field.kind] === true)
298
- return;
299
- addIssue(issues, path, "sort_kind_not_supported", `Provider does not support sort kind '${field.kind}'`);
300
374
  }
301
- function collectProviderMaxLimitIssues(limit, path, capabilities, issues) {
302
- const maxLimit = capabilities.limits?.maxLimit;
303
- if (maxLimit === undefined)
304
- return;
305
- if (!Number.isInteger(maxLimit) || maxLimit < 0) {
306
- addIssue(issues, path, "invalid_provider_limit_capability", "Provider limit capability maxLimit must be a non-negative integer");
375
+ function resolveAndValidatePredicateProperty(propertyId, op, shape, path, ctx) {
376
+ const properties = getPropertiesForResult(propertyId, shape, path, ctx);
377
+ if (properties.length === 0)
307
378
  return;
379
+ const schemas = [];
380
+ for (const { objectType, property } of properties) {
381
+ const schema = resolveSchemaForProperty(property, objectType.id, ctx, path);
382
+ if (schema)
383
+ schemas.push(schema);
384
+ if (isPrimaryExactPredicate(property, op)) {
385
+ continue;
386
+ }
387
+ if (property.query?.searchable !== true || property.query.filterable !== true) {
388
+ addIssue(ctx, path, "property_not_filterable", `Property '${propertyId}' on '${objectType.id}' must set query.searchable: true and query.filterable: true before it can be used in predicates`);
389
+ continue;
390
+ }
391
+ if (!schema)
392
+ continue;
393
+ if ((op === "lt" || op === "lte" || op === "gt" || op === "gte") && !isSortableSchema(schema)) {
394
+ addIssue(ctx, path, "operator_not_supported_for_schema", `Predicate '${op}' cannot be used with property '${propertyId}' on '${objectType.id}' because its schema is not orderable`);
395
+ }
396
+ if ((op === "eq" || op === "neq" || op === "in" || op === "exists") && !isExactSchema(schema)) {
397
+ addIssue(ctx, path, "operator_not_supported_for_schema", `Predicate '${op}' cannot be used with property '${propertyId}' on '${objectType.id}' because its schema cannot be exact-matched`);
398
+ }
399
+ if (op === "contains" && !isContainsSchema(schema)) {
400
+ addIssue(ctx, path, "operator_not_supported_for_schema", `Predicate 'contains' cannot be used with property '${propertyId}' on '${objectType.id}' because its schema is not string, array, or map`);
401
+ }
308
402
  }
309
- if (limit > maxLimit) {
310
- addIssue(issues, path, "provider_limit_too_large", `Provider supports limit up to ${maxLimit}`);
311
- }
312
- }
313
- function collectProviderMaxPageSizeIssues(pageSize, path, capabilities, issues) {
314
- const maxPageSize = capabilities.limits?.maxPageSize;
315
- if (maxPageSize === undefined)
316
- return;
317
- if (!Number.isInteger(maxPageSize) || maxPageSize <= 0) {
318
- addIssue(issues, path, "invalid_provider_page_size_capability", "Provider limit capability maxPageSize must be a positive integer");
403
+ if (op === "exists")
319
404
  return;
320
- }
321
- if (pageSize > maxPageSize) {
322
- addIssue(issues, path, "provider_page_size_too_large", `Provider supports pageSize up to ${maxPageSize}`);
323
- }
324
- }
325
- function collectFallbackIssues(query, options) {
326
- const issues = [];
327
- if (options.allowFallback === false) {
328
- addIssue(issues, "$", "fallback_disabled", "Object query fallback is disabled");
329
- return issues;
330
- }
331
- const maxFallbackRows = options.maxFallbackRows ?? DEFAULT_MAX_FALLBACK_ROWS;
332
- if (!Number.isInteger(maxFallbackRows) || maxFallbackRows <= 0) {
333
- addIssue(issues, "$", "invalid_fallback_bound", "maxFallbackRows must be a positive integer");
334
- }
335
- if (options.requiresExplicitFallbackBound !== false && !hasExplicitResultBound(query)) {
336
- addIssue(issues, "$", "fallback_requires_bound", "Fallback requires an explicit limit or page node");
337
- }
338
- collectFallbackNodeIssues(query, "$", issues);
339
- return issues;
405
+ if (op === "contains")
406
+ return resolveArrayItemScalarKind(schemas, ctx);
407
+ return resolveCommonQueryScalarKind(schemas, propertyId, path, ctx);
340
408
  }
341
- function collectFallbackNodeIssues(query, path, issues) {
342
- if (!fallbackNodeKinds.has(query.kind)) {
343
- addIssue(issues, path, "fallback_node_not_supported", `Fallback does not support query node '${query.kind}'`);
344
- }
345
- switch (query.kind) {
346
- case "start":
347
- case "refs":
348
- return;
349
- case "filter":
350
- case "limit":
351
- case "page":
352
- case "project":
353
- case "expand":
354
- collectFallbackNodeIssues(query.input, `${path}.input`, issues);
355
- return;
356
- case "sort":
357
- query.fields.forEach((field, index) => {
358
- if (field.kind === "relevance") {
359
- addIssue(issues, `${path}.fields[${index}]`, "fallback_sort_kind_not_supported", "Fallback does not support relevance sorting");
360
- }
361
- });
362
- collectFallbackNodeIssues(query.input, `${path}.input`, issues);
363
- return;
364
- case "text":
365
- case "vector":
366
- case "traverse":
367
- collectFallbackNodeIssues(query.input, `${path}.input`, issues);
368
- return;
369
- case "set":
370
- query.inputs.forEach((input, index) => {
371
- collectFallbackNodeIssues(input, `${path}.inputs[${index}]`, issues);
372
- });
409
+ function resolveArrayItemScalarKind(schemas, ctx) {
410
+ const kinds = new Set;
411
+ for (const schema of schemas) {
412
+ if (typeof schema === "string" || schema.type !== "array")
373
413
  return;
414
+ const items = resolveValueTypeRefs(schema.items, ctx.valueTypesById);
415
+ kinds.add(items ? queryScalarKindForSchema(items) : undefined);
374
416
  }
417
+ return kinds.size === 1 ? [...kinds][0] : undefined;
375
418
  }
376
- function hasExplicitResultBound(query) {
377
- switch (query.kind) {
378
- case "limit":
379
- case "page":
380
- case "refs":
381
- return true;
382
- case "start":
383
- return false;
384
- case "set":
385
- return query.inputs.some(hasExplicitResultBound);
386
- default:
387
- return hasExplicitResultBound(query.input);
388
- }
389
- }
390
- function addIssue(issues, path, code, message) {
391
- issues.push({ path, code, message });
392
- }
393
-
394
- // src/objects/query/validate.ts
395
- var DEFAULT_MAX_LIMIT = 1000;
396
- var DEFAULT_MAX_PAGE_SIZE = 1000;
397
- var DEFAULT_MAX_REFS = 1000;
398
- var NOOP_ADMISSION_STATE = Object.freeze({});
399
- var NOOP_QUERY_ADMISSION = Object.freeze({
400
- empty: () => NOOP_ADMISSION_STATE,
401
- source: () => ({ state: NOOP_ADMISSION_STATE }),
402
- property: () => {
419
+ function resolveValueTypeRefs(schema, valueTypesById, seen = new Set) {
420
+ if (typeof schema === "string" || schema.type !== "valueTypeRef")
421
+ return schema;
422
+ if (seen.has(schema.valueTypeId))
403
423
  return;
404
- },
405
- edge: (input) => ({
406
- state: input.state
407
- }),
408
- set: (input) => input.states[0] ?? NOOP_ADMISSION_STATE
409
- });
410
- function validateObjectQuery2(query, options) {
411
- const admitted = validateObjectQueryWithAdmission(query, options, NOOP_QUERY_ADMISSION);
412
- return {
413
- query: admitted.query,
414
- result: admitted.result,
415
- touchedObjectTypeIds: admitted.touchedObjectTypeIds
416
- };
417
- }
418
- function validateObjectQueryWithAdmission(query, options, admission) {
419
- const normalized = options.normalize === false ? query : normalizeObjectQuery2(query);
420
- const ctx = createValidationContext(options, admission);
421
- validateVectorComposition(query, ctx);
422
- const validation = validateQueryNode(normalized, "$", ctx);
423
- if (ctx.issues.length > 0) {
424
- throw new ObjectQueryValidationError2(ctx.issues);
425
- }
426
- const denial = ctx.admissionDenials[0];
427
- if (denial)
428
- throw denial;
429
- return {
430
- query: validation.query,
431
- result: validation.result,
432
- touchedObjectTypeIds: [...ctx.touchedObjectTypeIds],
433
- admissionState: validation.admissionState
434
- };
435
- }
436
- function collectObjectQueryValidationIssues2(query, options) {
437
- const normalized = options.normalize === false ? query : normalizeObjectQuery2(query);
438
- const ctx = createValidationContext(options, NOOP_QUERY_ADMISSION);
439
- validateVectorComposition(query, ctx);
440
- validateQueryNode(normalized, "$", ctx);
441
- return ctx.issues;
442
- }
443
- function resolveObjectQueryResultShape2(query, options) {
444
- return validateObjectQuery2(query, options).result;
424
+ seen.add(schema.valueTypeId);
425
+ const resolved = schema._resolved ?? valueTypesById.get(schema.valueTypeId)?.schema;
426
+ return resolved ? resolveValueTypeRefs(resolved, valueTypesById, seen) : undefined;
445
427
  }
446
- function validateVectorComposition(query, ctx) {
447
- if (hasVectorProfile2(query) && !isVectorProfileQuery2(query))
448
- ctx.issues.push({
449
- path: "$",
450
- code: "vector.composition",
451
- message: "Vector profiles support one top-k over one concrete type, with filters before ranking and optional limit/project after it."
452
- });
428
+ function normalizeObjectQueryValue(value, scalarKind) {
429
+ if (scalarKind !== "decimal" || typeof value !== "string")
430
+ return value;
431
+ try {
432
+ return normalizeDecimalValue2(value);
433
+ } catch {
434
+ return value;
435
+ }
453
436
  }
454
- function createValidationContext(options, admission) {
455
- return {
456
- ontology: options.ontology,
457
- valueTypesById: options.ontology.getValueTypesById(),
458
- maxLimit: options.maxLimit ?? DEFAULT_MAX_LIMIT,
459
- maxPageSize: options.maxPageSize ?? DEFAULT_MAX_PAGE_SIZE,
460
- maxRefs: options.maxRefs ?? DEFAULT_MAX_REFS,
461
- issues: [],
462
- touchedObjectTypeIds: new Set,
463
- admission,
464
- admissionDenials: []
465
- };
437
+ function resolveCommonQueryScalarKind(schemas, propertyId, path, ctx) {
438
+ const scalarKinds = schemas.map(queryScalarKindForSchema);
439
+ if (scalarKinds.length === 0 || scalarKinds.some((kind) => kind === undefined)) {
440
+ return;
441
+ }
442
+ const resolvedKinds = new Set(scalarKinds);
443
+ if (resolvedKinds.size === 1)
444
+ return scalarKinds[0];
445
+ addIssue(ctx, path, "incompatible_property_scalar_kinds", `Property '${propertyId}' resolves to incompatible scalar schemas: ${[...resolvedKinds].join(", ")}`);
446
+ return;
466
447
  }
467
- function validateQueryNode(query, path, ctx) {
468
- const validation = dispatchQueryNode(query, path, ctx);
469
- for (const objectTypeId of validation.result.objectTypeIds) {
470
- ctx.touchedObjectTypeIds.add(objectTypeId);
448
+ function validatePredicateValue(propertyId, value, shape, path, ctx) {
449
+ for (const { objectType, property } of getPropertiesForResult(propertyId, shape, path, ctx, {
450
+ reportMissing: false
451
+ })) {
452
+ try {
453
+ validatePropertyValue2(property, value, `${objectType.id}.${property.id}`, ctx.valueTypesById);
454
+ } catch (error) {
455
+ addIssue(ctx, path, "invalid_predicate_value", error instanceof Error ? error.message : `Invalid value for property '${property.id}' on '${objectType.id}'`);
456
+ }
471
457
  }
472
- return validation;
473
458
  }
474
- function dispatchQueryNode(query, path, ctx) {
475
- switch (query.kind) {
476
- case "start": {
477
- const validation = validateStart(query.objectTypeId, query.includeSubtypes, path, ctx);
478
- return {
479
- ...validation,
480
- admissionState: admitTransition(ctx, ctx.admission.source({
481
- kind: "start",
482
- result: validation.result,
483
- objectTypeId: query.objectTypeId,
484
- path
485
- }))
486
- };
459
+ function validateContainsValue(propertyId, value, shape, path, ctx) {
460
+ for (const { objectType, property } of getPropertiesForResult(propertyId, shape, path, ctx, {
461
+ reportMissing: false
462
+ })) {
463
+ const schema = resolveSchemaForProperty(property, objectType.id, ctx, path);
464
+ if (!schema)
465
+ continue;
466
+ if (typeof schema === "string") {
467
+ if (isContainsSchema(schema)) {
468
+ if (typeof value !== "string") {
469
+ addIssue(ctx, path, "invalid_contains_value", `Predicate 'contains' value for '${objectType.id}.${property.id}' must be a string`);
470
+ }
471
+ }
472
+ continue;
487
473
  }
488
- case "refs": {
489
- const validation = validateRefs(query.refs, path, ctx);
490
- return {
491
- ...validation,
492
- admissionState: admitTransition(ctx, ctx.admission.source({
493
- kind: "refs",
494
- result: validation.result,
495
- refs: validation.query.kind === "refs" ? validation.query.refs : query.refs,
496
- path
497
- }))
498
- };
474
+ if (schema.type === "array") {
475
+ try {
476
+ validateSchemaValue2(schema.items, value, `${objectType.id}.${property.id}[]`, ctx.valueTypesById);
477
+ } catch (error) {
478
+ addIssue(ctx, path, "invalid_contains_value", error instanceof Error ? error.message : `Invalid contains value for '${objectType.id}.${property.id}'`);
479
+ }
480
+ continue;
499
481
  }
500
- case "filter": {
501
- const input = validateQueryNode(query.input, `${path}.input`, ctx);
502
- const predicate = validatePredicate(query.predicate, input.result, input.admissionState, `${path}.predicate`, ctx);
503
- return {
504
- result: input.result,
505
- query: { ...query, input: input.query, predicate },
506
- admissionState: input.admissionState
507
- };
482
+ if (schema.type === "map" && typeof value !== "string") {
483
+ addIssue(ctx, path, "invalid_contains_value", `Predicate 'contains' value for '${objectType.id}.${property.id}' must be a map key string`);
508
484
  }
509
- case "text": {
510
- const input = validateQueryNode(query.input, `${path}.input`, ctx);
511
- const textFields = validateTextQuery(query.query, query.fields, input.result, input.admissionState, path, ctx);
512
- return {
513
- result: input.result,
514
- query: {
515
- ...query,
516
- input: input.query,
517
- fields: query.fields,
518
- fieldsByObjectType: query.fields ? undefined : textFields.fieldsByObjectType
519
- },
520
- admissionState: input.admissionState
521
- };
485
+ }
486
+ }
487
+ function validateTextQuery(query, fields, shape, admissionState, path, ctx) {
488
+ if (query.trim().length === 0) {
489
+ addIssue(ctx, path, "empty_text_query", "Text query must not be empty");
490
+ }
491
+ const fieldsByObjectType = fields ? undefined : {};
492
+ for (const objectType of getObjectTypesForResult(shape, ctx)) {
493
+ const fieldIds = fields ?? objectType.search?.defaultText;
494
+ if (!fieldIds || fieldIds.length === 0) {
495
+ addIssue(ctx, path, "missing_text_fields", `Object type '${objectType.id}' must define search.defaultText or the text query must specify fields`);
496
+ continue;
522
497
  }
523
- case "vector": {
524
- const input = validateQueryNode(query.input, `${path}.input`, ctx);
525
- return {
526
- result: input.result,
527
- query: validateVectorProfileQuery({ ...query, input: input.query }, input.result, input.admissionState, path, ctx),
528
- admissionState: input.admissionState
529
- };
498
+ if (fieldsByObjectType)
499
+ fieldsByObjectType[objectType.id] = uniqueStrings(fieldIds);
500
+ for (const fieldId of fieldIds) {
501
+ admitProperty(ctx, {
502
+ state: admissionState,
503
+ propertyId: fieldId,
504
+ objectTypeId: objectType.id,
505
+ use: "text",
506
+ path: `${path}.fields`
507
+ });
508
+ const property = getProperty(objectType, fieldId);
509
+ if (!property) {
510
+ addIssue(ctx, path, "unknown_text_field", `Text query references unknown property '${fieldId}' on '${objectType.id}'`);
511
+ continue;
512
+ }
513
+ assertStaticSearchProperty(objectType.id, property, "text query", path, ctx);
514
+ assertQueryFlag(objectType.id, property, "text", "text query", path, ctx);
515
+ const schema = resolveSchemaForProperty(property, objectType.id, ctx, path);
516
+ if (schema && !isTextSchema(schema)) {
517
+ addIssue(ctx, path, "text_field_not_string_like", `Text query field '${fieldId}' on '${objectType.id}' must be string-like`);
518
+ }
530
519
  }
531
- case "traverse": {
532
- const input = validateQueryNode(query.input, `${path}.input`, ctx);
533
- const result = validateTraverse(query.linkId, query.direction, query.sourceObjectTypeId, input.result, path, ctx);
534
- return {
535
- result,
536
- query: { ...query, input: input.query },
537
- admissionState: admitTransition(ctx, ctx.admission.edge({
538
- state: input.admissionState,
539
- result,
540
- linkId: query.linkId,
541
- direction: query.direction,
542
- sourceObjectTypeId: query.sourceObjectTypeId,
543
- use: "traverse",
544
- path
545
- }))
546
- };
520
+ }
521
+ return fieldsByObjectType ? { fieldsByObjectType } : {};
522
+ }
523
+ function validateVectorProfileQuery(query, shape, state, path, ctx) {
524
+ const types = getObjectTypesForResult(shape, ctx);
525
+ if (types.length !== 1 || !Number.isSafeInteger(query.k) || query.k < 1 || query.k > MAX_VECTOR_K) {
526
+ addIssue(ctx, path, "invalid_vector_profile_query", `Vector search requires one type, one profile and k between 1 and ${MAX_VECTOR_K}`);
527
+ return query;
528
+ }
529
+ const objectType = types[0];
530
+ const profile = query.profile === undefined ? undefined : objectType.search?.vectors?.[query.profile];
531
+ if (!profile) {
532
+ addIssue(ctx, path, "unknown_vector_profile", `Unknown vector profile: ${query.profile}`);
533
+ return query;
534
+ }
535
+ for (const propertyId of profile.source)
536
+ admitProperty(ctx, { state, propertyId, objectTypeId: objectType.id, use: "vector", path });
537
+ let values = query.vector;
538
+ try {
539
+ if (typeof values === "string") {
540
+ if (!values.trim() || values.length > 8000) {
541
+ throw new Error("Vector search text must be nonempty and at most 8000 characters.");
542
+ }
543
+ } else {
544
+ values = normalizeVector2(values, profile.model.definition.dimensions);
547
545
  }
548
- case "set":
549
- return validateSet(query.inputs, query.op, path, ctx);
550
- case "sort": {
551
- const input = validateQueryNode(query.input, `${path}.input`, ctx);
552
- const fields = validateSortFields(query.fields, input.result, input.admissionState, "sort", path, ctx);
553
- return {
554
- result: input.result,
555
- query: { ...query, input: input.query, fields },
556
- admissionState: input.admissionState
557
- };
546
+ } catch (error) {
547
+ addIssue(ctx, path, "invalid_vector", error instanceof Error ? error.message : "Invalid vector");
548
+ }
549
+ return {
550
+ ...query,
551
+ vector: values,
552
+ source: [...profile.source],
553
+ configuration: vectorConfiguration(profile)
554
+ };
555
+ }
556
+ function validateTraverse(linkId, direction, sourceObjectTypeId, shape, path, ctx) {
557
+ if (!linkId) {
558
+ addIssue(ctx, path, "missing_link_id", "traverse.linkId is required");
559
+ return { objectTypeIds: [] };
560
+ }
561
+ if (direction === "outgoing") {
562
+ if (sourceObjectTypeId !== undefined) {
563
+ addIssue(ctx, path, "traverse_source_not_applicable", "traverse.sourceObjectTypeId only applies to incoming traversal");
558
564
  }
559
- case "limit": {
560
- const input = validateQueryNode(query.input, `${path}.input`, ctx);
561
- validateLimit(query.limit, path, ctx);
562
- return {
563
- result: input.result,
564
- query: { ...query, input: input.query },
565
- admissionState: input.admissionState
566
- };
565
+ return validateOutgoingTraverse(linkId, shape, path, ctx);
566
+ }
567
+ return validateIncomingTraverse(linkId, sourceObjectTypeId, shape, path, ctx);
568
+ }
569
+ function validateOutgoingTraverse(linkId, shape, path, ctx) {
570
+ const targetTypeIds = [];
571
+ for (const objectType of getObjectTypesForResult(shape, ctx)) {
572
+ const link = objectType.links.find((candidate) => candidate.id === linkId);
573
+ if (!link) {
574
+ addIssue(ctx, path, "unknown_link", `Outgoing traversal references unknown link '${linkId}' on '${objectType.id}'`);
575
+ continue;
567
576
  }
568
- case "page": {
569
- const input = validateQueryNode(query.input, `${path}.input`, ctx);
570
- validatePage(query.pageSize, query.pageToken, path, ctx);
571
- return {
572
- result: input.result,
573
- query: { ...query, input: input.query },
574
- admissionState: input.admissionState
575
- };
577
+ if (link.targetObjectTypeId === "*") {
578
+ addIssue(ctx, path, "wildcard_traverse_target", `Outgoing traversal over wildcard link '${objectType.id}.${linkId}' cannot infer a target object type`);
579
+ continue;
576
580
  }
577
- case "project": {
578
- const input = validateQueryNode(query.input, `${path}.input`, ctx);
579
- validateProjection(query.properties, input.result, input.admissionState, path, ctx);
580
- return {
581
- result: input.result,
582
- query: { ...query, input: input.query },
583
- admissionState: input.admissionState
584
- };
581
+ const declaredTargets = Array.isArray(link.targetObjectTypeId) ? link.targetObjectTypeId : [link.targetObjectTypeId];
582
+ for (const targetId of declaredTargets) {
583
+ if (!ctx.ontology.getObjectTypeById(targetId)) {
584
+ addIssue(ctx, path, "unknown_link_target", `Outgoing traversal over '${objectType.id}.${linkId}' targets unknown object type '${targetId}'`);
585
+ } else {
586
+ targetTypeIds.push(targetId);
587
+ }
585
588
  }
586
- case "expand": {
587
- const input = validateQueryNode(query.input, `${path}.input`, ctx);
588
- const expansions = validateExpansions(query.expansions, input.result, input.admissionState, `${path}.expansions`, ctx);
589
- return {
590
- result: input.result,
591
- query: { ...query, input: input.query, expansions },
592
- admissionState: input.admissionState
593
- };
589
+ }
590
+ return { objectTypeIds: uniqueStrings(targetTypeIds) };
591
+ }
592
+ function validateIncomingTraverse(linkId, sourceObjectTypeId, shape, path, ctx) {
593
+ let candidateTypes = ctx.ontology.listObjectTypes();
594
+ if (sourceObjectTypeId !== undefined) {
595
+ const sourceType = ctx.ontology.getObjectTypeById(sourceObjectTypeId);
596
+ if (!sourceType) {
597
+ addIssue(ctx, path, "unknown_traverse_source", `Incoming traversal references unknown source object type '${sourceObjectTypeId}'`);
598
+ return { objectTypeIds: [] };
594
599
  }
600
+ candidateTypes = [sourceType];
595
601
  }
596
- }
597
- function validateRefs(refs, path, ctx) {
598
- if (refs.length === 0) {
599
- addIssue2(ctx, `${path}.refs`, "empty_refs", "refs must include at least one object reference");
602
+ const sourceTypeIds = [];
603
+ for (const sourceType of candidateTypes) {
604
+ const link = sourceType.links.find((candidate) => candidate.id === linkId);
605
+ if (!link)
606
+ continue;
607
+ if (link.targetObjectTypeId === "*") {
608
+ sourceTypeIds.push(sourceType.id);
609
+ continue;
610
+ }
611
+ const matches = shape.objectTypeIds.some((targetTypeId) => ctx.ontology.isValidLinkTarget(link.targetObjectTypeId, targetTypeId));
612
+ if (matches)
613
+ sourceTypeIds.push(sourceType.id);
600
614
  }
601
- if (refs.length > ctx.maxRefs) {
602
- addIssue2(ctx, `${path}.refs`, "too_many_refs", `refs must include at most ${ctx.maxRefs} object references`);
615
+ if (sourceTypeIds.length === 0) {
616
+ addIssue(ctx, path, "incoming_link_not_found", `Incoming traversal found no source object types with link '${linkId}' targeting ${shape.objectTypeIds.join(" | ")}`);
603
617
  }
604
- const objectTypeIds = new Set;
605
- refs.forEach((ref, index) => {
606
- const refPath = `${path}.refs[${index}]`;
607
- if (!ref.objectTypeId) {
608
- addIssue2(ctx, `${refPath}.objectTypeId`, "missing_object_type", "objectTypeId is required");
609
- } else if (!ctx.ontology.getObjectTypeById(ref.objectTypeId)) {
610
- addIssue2(ctx, `${refPath}.objectTypeId`, "unknown_object_type", formatUnknownObjectTypeMessage(ref.objectTypeId));
611
- } else {
612
- objectTypeIds.add(ref.objectTypeId);
613
- }
614
- if (!ref.primaryId) {
615
- addIssue2(ctx, `${refPath}.primaryId`, "missing_primary_id", "primaryId is required");
616
- }
617
- });
618
- return {
619
- result: { objectTypeIds: [...objectTypeIds] },
620
- query: { kind: "refs", refs: refs.map((ref) => ({ ...ref })) }
621
- };
618
+ return { objectTypeIds: uniqueStrings(sourceTypeIds) };
622
619
  }
623
- function validateStart(objectTypeId, includeSubtypes, path, ctx) {
624
- if (!objectTypeId) {
625
- addIssue2(ctx, path, "missing_object_type", "start.objectTypeId is required");
626
- return { result: { objectTypeIds: [] }, query: { kind: "start", objectTypeId } };
620
+ function validateExpansions(expansions, shape, admissionState, path, ctx) {
621
+ return expansions.map((expansion, index) => validateExpansion(expansion, shape, admissionState, `${path}[${index}]`, ctx));
622
+ }
623
+ function validateExpansion(expansion, shape, admissionState, path, ctx) {
624
+ if (!expansion.linkId) {
625
+ addIssue(ctx, path, "missing_expand_link", "expand.linkId is required");
626
+ return expansion;
627
627
  }
628
- if (!ctx.ontology.getObjectTypeById(objectTypeId)) {
629
- addIssue2(ctx, path, "unknown_object_type", formatUnknownObjectTypeMessage(objectTypeId));
630
- return { result: { objectTypeIds: [] }, query: { kind: "start", objectTypeId } };
628
+ const targetShape = expansion.direction === "incoming" ? resolveIncomingExpansionTargets(expansion.linkId, expansion.sourceObjectTypeId, shape, path, ctx) : resolveOutgoingExpansionTargets(expansion.linkId, expansion.sourceObjectTypeId, shape, path, ctx);
629
+ const targetAdmissionState = admitTransition(ctx, ctx.admission.edge({
630
+ state: admissionState,
631
+ result: targetShape,
632
+ linkId: expansion.linkId,
633
+ direction: expansion.direction,
634
+ sourceObjectTypeId: expansion.sourceObjectTypeId,
635
+ use: "expand",
636
+ path
637
+ }));
638
+ for (const targetTypeId of targetShape.objectTypeIds) {
639
+ ctx.touchedObjectTypeIds.add(targetTypeId);
631
640
  }
632
- const objectTypeIds = includeSubtypes ? uniqueStrings([objectTypeId, ...ctx.ontology.listSubTypes(objectTypeId)]) : [objectTypeId];
633
- return { result: { objectTypeIds }, query: { kind: "start", objectTypeId, includeSubtypes } };
641
+ validateExpansionLimit(expansion.limit, path, ctx);
642
+ const orderBy = expansion.orderBy ? validateSortFields(expansion.orderBy, targetShape, targetAdmissionState, "expand.orderBy", `${path}.orderBy`, ctx) : undefined;
643
+ const expand = expansion.expand ? validateExpansions(expansion.expand, targetShape, targetAdmissionState, `${path}.expand`, ctx) : undefined;
644
+ return { ...expansion, orderBy, expand };
634
645
  }
635
- function validatePredicate(predicate, shape, admissionState, path, ctx) {
636
- switch (predicate.op) {
637
- case "and":
638
- case "or":
639
- if (predicate.items.length === 0) {
640
- addIssue2(ctx, path, "empty_predicate_group", `Predicate '${predicate.op}' must not be empty`);
641
- }
642
- return {
643
- ...predicate,
644
- items: predicate.items.map((item, index) => validatePredicate(item, shape, admissionState, `${path}.items[${index}]`, ctx))
645
- };
646
- case "not":
647
- return {
648
- ...predicate,
649
- item: validatePredicate(predicate.item, shape, admissionState, `${path}.item`, ctx)
650
- };
651
- case "eq":
652
- case "neq":
653
- case "lt":
654
- case "lte":
655
- case "gt":
656
- case "gte": {
657
- const { scalarKind: _authoredScalarKind, ...authoredPredicate } = predicate;
658
- const scalarKind = resolveAndValidatePredicateProperty(predicate.propertyId, predicate.op, shape, path, ctx);
659
- validatePredicateValue(predicate.propertyId, predicate.value, shape, path, ctx);
660
- admitProperty(ctx, {
661
- state: admissionState,
662
- propertyId: predicate.propertyId,
663
- use: "filter",
664
- path
665
- });
666
- return {
667
- ...authoredPredicate,
668
- value: normalizeObjectQueryValue(predicate.value, scalarKind),
669
- ...scalarKind ? { scalarKind } : {}
670
- };
671
- }
672
- case "in": {
673
- const { scalarKind: _authoredScalarKind, ...authoredPredicate } = predicate;
674
- const scalarKind = resolveAndValidatePredicateProperty(predicate.propertyId, predicate.op, shape, path, ctx);
675
- if (predicate.values.length === 0) {
676
- addIssue2(ctx, path, "empty_in_values", "Predicate 'in' must include at least one value");
677
- }
678
- predicate.values.forEach((value, index) => {
679
- validatePredicateValue(predicate.propertyId, value, shape, `${path}.values[${index}]`, ctx);
680
- });
681
- admitProperty(ctx, {
682
- state: admissionState,
683
- propertyId: predicate.propertyId,
684
- use: "filter",
685
- path
686
- });
687
- return {
688
- ...authoredPredicate,
689
- values: predicate.values.map((value) => normalizeObjectQueryValue(value, scalarKind)),
690
- ...scalarKind ? { scalarKind } : {}
691
- };
646
+ function resolveOutgoingExpansionTargets(linkId, sourceObjectTypeId, shape, path, ctx) {
647
+ if (sourceObjectTypeId !== undefined) {
648
+ addIssue(ctx, path, "expand_source_not_applicable", "expand.sourceObjectTypeId only applies to incoming expansion");
649
+ }
650
+ const targetTypeIds = [];
651
+ let foundOnAnyType = false;
652
+ for (const objectType of getObjectTypesForResult(shape, ctx)) {
653
+ const link = objectType.links.find((candidate) => candidate.id === linkId);
654
+ if (!link)
655
+ continue;
656
+ foundOnAnyType = true;
657
+ if (link.targetObjectTypeId === "*") {
658
+ addIssue(ctx, path, "wildcard_expand_target", `Expansion over wildcard link '${objectType.id}.${linkId}' cannot infer a target object type`);
659
+ continue;
692
660
  }
693
- case "exists":
694
- resolveAndValidatePredicateProperty(predicate.propertyId, predicate.op, shape, path, ctx);
695
- admitProperty(ctx, {
696
- state: admissionState,
697
- propertyId: predicate.propertyId,
698
- use: "filter",
699
- path
700
- });
701
- if (typeof predicate.value !== "boolean") {
702
- addIssue2(ctx, path, "invalid_exists_value", "Predicate 'exists' value must be boolean");
661
+ const declaredTargets = Array.isArray(link.targetObjectTypeId) ? link.targetObjectTypeId : [link.targetObjectTypeId];
662
+ for (const targetId of declaredTargets) {
663
+ if (!ctx.ontology.getObjectTypeById(targetId)) {
664
+ addIssue(ctx, path, "unknown_expand_target", `Expansion over '${objectType.id}.${linkId}' targets unknown object type '${targetId}'`);
665
+ } else {
666
+ targetTypeIds.push(targetId);
703
667
  }
704
- return predicate;
705
- case "contains": {
706
- const { scalarKind: _authoredScalarKind, ...authoredPredicate } = predicate;
707
- const scalarKind = resolveAndValidatePredicateProperty(predicate.propertyId, predicate.op, shape, path, ctx);
708
- validateContainsValue(predicate.propertyId, predicate.value, shape, path, ctx);
709
- admitProperty(ctx, {
710
- state: admissionState,
711
- propertyId: predicate.propertyId,
712
- use: "filter",
713
- path
714
- });
715
- return {
716
- ...authoredPredicate,
717
- value: normalizeObjectQueryValue(predicate.value, scalarKind),
718
- ...scalarKind ? { scalarKind } : {}
719
- };
720
668
  }
721
669
  }
670
+ if (!foundOnAnyType) {
671
+ addIssue(ctx, path, "unknown_expand_link", `Expansion references link '${linkId}' not present on result type ${shape.objectTypeIds.join(" | ")} (an expand abandoned by a later traverse?)`);
672
+ }
673
+ return { objectTypeIds: uniqueStrings(targetTypeIds) };
722
674
  }
723
- function resolveAndValidatePredicateProperty(propertyId, op, shape, path, ctx) {
724
- const properties = getPropertiesForResult(propertyId, shape, path, ctx);
725
- if (properties.length === 0)
726
- return;
727
- const schemas = [];
728
- for (const { objectType, property } of properties) {
729
- const schema = resolveSchemaForProperty(property, objectType.id, ctx, path);
730
- if (schema)
731
- schemas.push(schema);
732
- if (isPrimaryExactPredicate(property, op)) {
733
- continue;
675
+ function resolveIncomingExpansionTargets(linkId, sourceObjectTypeId, shape, path, ctx) {
676
+ let candidateTypes = ctx.ontology.listObjectTypes();
677
+ if (sourceObjectTypeId !== undefined) {
678
+ const sourceType = ctx.ontology.getObjectTypeById(sourceObjectTypeId);
679
+ if (!sourceType) {
680
+ addIssue(ctx, path, "unknown_expand_source", `Incoming expansion references unknown source object type '${sourceObjectTypeId}'`);
681
+ return { objectTypeIds: [] };
734
682
  }
735
- if (property.query?.searchable !== true || property.query.filterable !== true) {
736
- addIssue2(ctx, path, "property_not_filterable", `Property '${propertyId}' on '${objectType.id}' must set query.searchable: true and query.filterable: true before it can be used in predicates`);
683
+ candidateTypes = [sourceType];
684
+ }
685
+ const sourceTypeIds = [];
686
+ for (const sourceType of candidateTypes) {
687
+ const link = sourceType.links.find((candidate) => candidate.id === linkId);
688
+ if (!link)
737
689
  continue;
738
- }
739
- if (!schema)
690
+ if (link.targetObjectTypeId === "*") {
691
+ sourceTypeIds.push(sourceType.id);
740
692
  continue;
741
- if ((op === "lt" || op === "lte" || op === "gt" || op === "gte") && !isSortableSchema(schema)) {
742
- addIssue2(ctx, path, "operator_not_supported_for_schema", `Predicate '${op}' cannot be used with property '${propertyId}' on '${objectType.id}' because its schema is not orderable`);
743
693
  }
744
- if ((op === "eq" || op === "neq" || op === "in" || op === "exists") && !isExactSchema(schema)) {
745
- addIssue2(ctx, path, "operator_not_supported_for_schema", `Predicate '${op}' cannot be used with property '${propertyId}' on '${objectType.id}' because its schema cannot be exact-matched`);
694
+ const matches = shape.objectTypeIds.some((targetTypeId) => ctx.ontology.isValidLinkTarget(link.targetObjectTypeId, targetTypeId));
695
+ if (matches)
696
+ sourceTypeIds.push(sourceType.id);
697
+ }
698
+ if (sourceTypeIds.length === 0) {
699
+ addIssue(ctx, path, "incoming_expand_link_not_found", `Incoming expansion found no source object types with link '${linkId}' targeting ${shape.objectTypeIds.join(" | ")}`);
700
+ }
701
+ return { objectTypeIds: uniqueStrings(sourceTypeIds) };
702
+ }
703
+ function validateExpansionLimit(limit, path, ctx) {
704
+ if (limit === undefined)
705
+ return;
706
+ if (!Number.isInteger(limit) || limit < 0) {
707
+ addIssue(ctx, `${path}.limit`, "invalid_expand_limit", "expand.limit must be a non-negative integer");
708
+ }
709
+ }
710
+ function validateSet(inputs, op, path, ctx) {
711
+ if (inputs.length === 0) {
712
+ addIssue(ctx, path, "empty_set", `Set operation '${op}' must include at least one input`);
713
+ return {
714
+ result: { objectTypeIds: [] },
715
+ query: { kind: "set", op, inputs },
716
+ admissionState: ctx.admission.empty()
717
+ };
718
+ }
719
+ const results = inputs.map((input, index) => validateQueryNode(input, `${path}.inputs[${index}]`, ctx));
720
+ const first = results[0]?.result ?? { objectTypeIds: [] };
721
+ const expected = keyForTypeIds(first.objectTypeIds);
722
+ results.forEach((result, index) => {
723
+ if (keyForTypeIds(result.result.objectTypeIds) !== expected) {
724
+ addIssue(ctx, `${path}.inputs[${index}]`, "incompatible_set_input", `Set operation '${op}' inputs must produce the same object type set`);
746
725
  }
747
- if (op === "contains" && !isContainsSchema(schema)) {
748
- addIssue2(ctx, path, "operator_not_supported_for_schema", `Predicate 'contains' cannot be used with property '${propertyId}' on '${objectType.id}' because its schema is not string, array, or map`);
726
+ });
727
+ return {
728
+ result: first,
729
+ query: { kind: "set", op, inputs: results.map((result) => result.query) },
730
+ admissionState: ctx.admission.set({
731
+ op,
732
+ states: results.map((result) => result.admissionState),
733
+ path
734
+ })
735
+ };
736
+ }
737
+ function validateSortFields(fields, shape, admissionState, use, path, ctx) {
738
+ if (fields.length === 0) {
739
+ addIssue(ctx, path, "empty_sort", "Sort must include at least one field");
740
+ return [];
741
+ }
742
+ const seen = new Set;
743
+ return fields.map((field, index) => {
744
+ if (field.direction && field.direction !== "asc" && field.direction !== "desc") {
745
+ addIssue(ctx, `${path}.fields[${index}]`, "invalid_sort_direction", "Sort direction must be asc or desc");
746
+ }
747
+ const key = field.kind === "property" ? `property:${field.propertyId}` : "relevance";
748
+ if (seen.has(key)) {
749
+ addIssue(ctx, `${path}.fields[${index}]`, "duplicate_sort_field", `Duplicate sort field '${key}'`);
750
+ }
751
+ seen.add(key);
752
+ if (field.kind === "relevance")
753
+ return field;
754
+ admitProperty(ctx, {
755
+ state: admissionState,
756
+ propertyId: field.propertyId,
757
+ use,
758
+ path: `${path}.fields[${index}]`
759
+ });
760
+ const { scalarKind: _authoredScalarKind, ...authoredField } = field;
761
+ const schemas = [];
762
+ for (const { objectType, property } of getPropertiesForResult(field.propertyId, shape, `${path}.fields[${index}]`, ctx)) {
763
+ if (property.query?.searchable !== true || property.query.sortable !== true) {
764
+ addIssue(ctx, `${path}.fields[${index}]`, "property_not_sortable", `Property '${property.id}' on '${objectType.id}' must set query.searchable: true and query.sortable: true before it can be sorted`);
765
+ continue;
766
+ }
767
+ const schema = resolveSchemaForProperty(property, objectType.id, ctx, `${path}.fields[${index}]`);
768
+ if (schema)
769
+ schemas.push(schema);
770
+ if (schema && !isSortableSchema(schema)) {
771
+ addIssue(ctx, `${path}.fields[${index}]`, "sort_field_not_orderable", `Sort field '${property.id}' on '${objectType.id}' is not orderable`);
772
+ }
749
773
  }
750
- }
751
- if (op === "exists")
752
- return;
753
- if (op === "contains")
754
- return resolveArrayItemScalarKind(schemas, ctx);
755
- return resolveCommonQueryScalarKind(schemas, propertyId, path, ctx);
774
+ const scalarKind = resolveCommonQueryScalarKind(schemas, field.propertyId, `${path}.fields[${index}]`, ctx);
775
+ return { ...authoredField, ...scalarKind ? { scalarKind } : {} };
776
+ });
756
777
  }
757
- function resolveArrayItemScalarKind(schemas, ctx) {
758
- const kinds = new Set;
759
- for (const schema of schemas) {
760
- if (typeof schema === "string" || schema.type !== "array")
761
- return;
762
- const items = resolveValueTypeRefs(schema.items, ctx.valueTypesById);
763
- kinds.add(items ? queryScalarKindForSchema(items) : undefined);
778
+ function validateLimit(limit, path, ctx) {
779
+ if (!Number.isInteger(limit) || limit < 0) {
780
+ addIssue(ctx, path, "invalid_limit", "limit must be a non-negative integer");
781
+ } else if (limit > ctx.maxLimit) {
782
+ addIssue(ctx, path, "limit_too_large", `limit must be <= ${ctx.maxLimit}`);
764
783
  }
765
- return kinds.size === 1 ? [...kinds][0] : undefined;
766
- }
767
- function resolveValueTypeRefs(schema, valueTypesById, seen = new Set) {
768
- if (typeof schema === "string" || schema.type !== "valueTypeRef")
769
- return schema;
770
- if (seen.has(schema.valueTypeId))
771
- return;
772
- seen.add(schema.valueTypeId);
773
- const resolved = schema._resolved ?? valueTypesById.get(schema.valueTypeId)?.schema;
774
- return resolved ? resolveValueTypeRefs(resolved, valueTypesById, seen) : undefined;
775
784
  }
776
- function normalizeObjectQueryValue(value, scalarKind) {
777
- if (scalarKind !== "decimal" || typeof value !== "string")
778
- return value;
779
- try {
780
- return normalizeDecimalValue2(value);
781
- } catch {
782
- return value;
785
+ function validatePage(pageSize, pageToken, path, ctx) {
786
+ if (!Number.isInteger(pageSize) || pageSize <= 0) {
787
+ addIssue(ctx, path, "invalid_page_size", "pageSize must be a positive integer");
788
+ } else if (pageSize > ctx.maxPageSize) {
789
+ addIssue(ctx, path, "page_size_too_large", `pageSize must be <= ${ctx.maxPageSize}`);
790
+ }
791
+ if (pageToken !== undefined && pageToken.length === 0) {
792
+ addIssue(ctx, path, "empty_page_token", "pageToken must not be empty when provided");
783
793
  }
784
794
  }
785
- function resolveCommonQueryScalarKind(schemas, propertyId, path, ctx) {
786
- const scalarKinds = schemas.map(queryScalarKindForSchema);
787
- if (scalarKinds.length === 0 || scalarKinds.some((kind) => kind === undefined)) {
795
+ function validateProjection(properties, shape, admissionState, path, ctx) {
796
+ if (!properties)
788
797
  return;
798
+ if (properties.length === 0) {
799
+ addIssue(ctx, path, "empty_projection", "project.properties must not be empty when provided");
789
800
  }
790
- const resolvedKinds = new Set(scalarKinds);
791
- if (resolvedKinds.size === 1)
792
- return scalarKinds[0];
793
- addIssue2(ctx, path, "incompatible_property_scalar_kinds", `Property '${propertyId}' resolves to incompatible scalar schemas: ${[...resolvedKinds].join(", ")}`);
794
- return;
795
- }
796
- function validatePredicateValue(propertyId, value, shape, path, ctx) {
797
- for (const { objectType, property } of getPropertiesForResult(propertyId, shape, path, ctx, {
798
- reportMissing: false
799
- })) {
800
- try {
801
- validatePropertyValue2(property, value, `${objectType.id}.${property.id}`, ctx.valueTypesById);
802
- } catch (error) {
803
- addIssue2(ctx, path, "invalid_predicate_value", error instanceof Error ? error.message : `Invalid value for property '${property.id}' on '${objectType.id}'`);
801
+ const seen = new Set;
802
+ properties.forEach((propertyId, index) => {
803
+ admitProperty(ctx, {
804
+ state: admissionState,
805
+ propertyId,
806
+ use: "project",
807
+ path: `${path}.properties[${index}]`
808
+ });
809
+ if (seen.has(propertyId)) {
810
+ addIssue(ctx, `${path}.properties[${index}]`, "duplicate_projection_property", `Duplicate projected property '${propertyId}'`);
804
811
  }
805
- }
812
+ seen.add(propertyId);
813
+ getPropertiesForResult(propertyId, shape, `${path}.properties[${index}]`, ctx);
814
+ });
806
815
  }
807
- function validateContainsValue(propertyId, value, shape, path, ctx) {
808
- for (const { objectType, property } of getPropertiesForResult(propertyId, shape, path, ctx, {
809
- reportMissing: false
810
- })) {
811
- const schema = resolveSchemaForProperty(property, objectType.id, ctx, path);
812
- if (!schema)
813
- continue;
814
- if (typeof schema === "string") {
815
- if (isContainsSchema(schema)) {
816
- if (typeof value !== "string") {
817
- addIssue2(ctx, path, "invalid_contains_value", `Predicate 'contains' value for '${objectType.id}.${property.id}' must be a string`);
818
- }
819
- }
820
- continue;
821
- }
822
- if (schema.type === "array") {
823
- try {
824
- validateSchemaValue2(schema.items, value, `${objectType.id}.${property.id}[]`, ctx.valueTypesById);
825
- } catch (error) {
826
- addIssue2(ctx, path, "invalid_contains_value", error instanceof Error ? error.message : `Invalid contains value for '${objectType.id}.${property.id}'`);
827
- }
828
- continue;
829
- }
830
- if (schema.type === "map" && typeof value !== "string") {
831
- addIssue2(ctx, path, "invalid_contains_value", `Predicate 'contains' value for '${objectType.id}.${property.id}' must be a map key string`);
832
- }
816
+ function getObjectTypesForResult(shape, ctx) {
817
+ const objectTypes = [];
818
+ for (const objectTypeId of shape.objectTypeIds) {
819
+ const objectType = ctx.ontology.getObjectTypeById(objectTypeId);
820
+ if (objectType)
821
+ objectTypes.push(objectType);
833
822
  }
823
+ return objectTypes;
834
824
  }
835
- function validateTextQuery(query, fields, shape, admissionState, path, ctx) {
836
- if (query.trim().length === 0) {
837
- addIssue2(ctx, path, "empty_text_query", "Text query must not be empty");
838
- }
839
- const fieldsByObjectType = fields ? undefined : {};
825
+ function getPropertiesForResult(propertyId, shape, path, ctx, options = {}) {
826
+ const results = [];
840
827
  for (const objectType of getObjectTypesForResult(shape, ctx)) {
841
- const fieldIds = fields ?? objectType.search?.defaultText;
842
- if (!fieldIds || fieldIds.length === 0) {
843
- addIssue2(ctx, path, "missing_text_fields", `Object type '${objectType.id}' must define search.defaultText or the text query must specify fields`);
844
- continue;
845
- }
846
- if (fieldsByObjectType)
847
- fieldsByObjectType[objectType.id] = uniqueStrings(fieldIds);
848
- for (const fieldId of fieldIds) {
849
- admitProperty(ctx, {
850
- state: admissionState,
851
- propertyId: fieldId,
852
- objectTypeId: objectType.id,
853
- use: "text",
854
- path: `${path}.fields`
855
- });
856
- const property = getProperty(objectType, fieldId);
857
- if (!property) {
858
- addIssue2(ctx, path, "unknown_text_field", `Text query references unknown property '${fieldId}' on '${objectType.id}'`);
828
+ const property = getProperty(objectType, propertyId);
829
+ if (!property) {
830
+ if (options.reportMissing === false) {
859
831
  continue;
860
832
  }
861
- assertStaticSearchProperty(objectType.id, property, "text query", path, ctx);
862
- assertQueryFlag(objectType.id, property, "text", "text query", path, ctx);
863
- const schema = resolveSchemaForProperty(property, objectType.id, ctx, path);
864
- if (schema && !isTextSchema(schema)) {
865
- addIssue2(ctx, path, "text_field_not_string_like", `Text query field '${fieldId}' on '${objectType.id}' must be string-like`);
866
- }
833
+ addIssue(ctx, path, "unknown_property", `Property '${propertyId}' does not exist on object type '${objectType.id}'`);
834
+ continue;
867
835
  }
836
+ results.push({ objectType, property });
868
837
  }
869
- return fieldsByObjectType ? { fieldsByObjectType } : {};
838
+ return results;
870
839
  }
871
- function validateVectorProfileQuery(query, shape, state, path, ctx) {
872
- const types = getObjectTypesForResult(shape, ctx);
873
- if (types.length !== 1 || !Number.isSafeInteger(query.k) || query.k < 1 || query.k > MAX_VECTOR_K) {
874
- addIssue2(ctx, path, "invalid_vector_profile_query", `Vector search requires one type, one profile and k between 1 and ${MAX_VECTOR_K}`);
875
- return query;
876
- }
877
- let input = query.input;
878
- while (input.kind === "filter")
879
- input = input.input;
880
- if (input.kind !== "start" || input.includeSubtypes)
881
- addIssue2(ctx, path, "unsupported_vector_composition", "Vector profile input must be a type with optional filters.");
882
- const objectType = types[0];
883
- const profile = query.profile === undefined ? undefined : objectType.search?.vectors?.[query.profile];
884
- if (!profile) {
885
- addIssue2(ctx, path, "unknown_vector_profile", `Unknown vector profile: ${query.profile}`);
886
- return query;
840
+ function getProperty(objectType, propertyId) {
841
+ return objectType.properties.find((property) => property.id === propertyId);
842
+ }
843
+ function assertStaticSearchProperty(objectTypeId, property, context, path, ctx) {
844
+ if (property.mode === "telemetry") {
845
+ addIssue(ctx, path, "telemetry_search_property", `${context} references telemetry property '${property.id}' on '${objectTypeId}'. Search can only reference static properties because telemetry latest values are not object-query indexed.`);
887
846
  }
888
- for (const propertyId of profile.source)
889
- admitProperty(ctx, { state, propertyId, objectTypeId: objectType.id, use: "vector", path });
890
- let values = query.vector;
847
+ }
848
+ function assertQueryFlag(objectTypeId, property, flag, context, path, ctx) {
849
+ if (property.query?.searchable === true && property.query[flag] === true)
850
+ return;
851
+ addIssue(ctx, path, "query_field_not_enabled", `${context} field '${property.id}' on '${objectTypeId}' must set query.searchable: true and query.${flag}: true`);
852
+ }
853
+ function resolveSchemaForProperty(property, objectTypeId, ctx, path) {
891
854
  try {
892
- if (typeof values === "string") {
893
- if (!values.trim() || values.length > 8000) {
894
- throw new Error("Vector search text must be nonempty and at most 8000 characters.");
895
- }
896
- } else {
897
- values = normalizeVector2(values, profile.model.definition.dimensions);
898
- }
855
+ return resolveSchema(property.schema, ctx.valueTypesById);
899
856
  } catch (error) {
900
- addIssue2(ctx, path, "invalid_vector", error instanceof Error ? error.message : "Invalid vector");
857
+ addIssue(ctx, path, "unresolved_property_schema", error instanceof Error ? error.message : `Could not resolve schema for '${objectTypeId}.${property.id}'`);
858
+ return null;
901
859
  }
902
- return {
903
- ...query,
904
- vector: values,
905
- source: [...profile.source],
906
- configuration: vectorConfiguration(profile)
907
- };
908
860
  }
909
- function validateTraverse(linkId, direction, sourceObjectTypeId, shape, path, ctx) {
910
- if (!linkId) {
911
- addIssue2(ctx, path, "missing_link_id", "traverse.linkId is required");
912
- return { objectTypeIds: [] };
861
+ function resolveSchema(schema, valueTypesById, seenValueTypeIds = new Set) {
862
+ if (typeof schema === "string")
863
+ return schema;
864
+ if (schema.type !== "valueTypeRef")
865
+ return schema;
866
+ if (seenValueTypeIds.has(schema.valueTypeId)) {
867
+ throw new Error(`Circular valueTypeRef '${schema.valueTypeId}'`);
913
868
  }
914
- if (direction === "outgoing") {
915
- if (sourceObjectTypeId !== undefined) {
916
- addIssue2(ctx, path, "traverse_source_not_applicable", "traverse.sourceObjectTypeId only applies to incoming traversal");
917
- }
918
- return validateOutgoingTraverse(linkId, shape, path, ctx);
869
+ const resolved = schema._resolved ?? valueTypesById.get(schema.valueTypeId)?.schema;
870
+ if (!resolved) {
871
+ throw new Error(`Unknown valueTypeRef '${schema.valueTypeId}'`);
919
872
  }
920
- return validateIncomingTraverse(linkId, sourceObjectTypeId, shape, path, ctx);
873
+ seenValueTypeIds.add(schema.valueTypeId);
874
+ return resolveSchema(resolved, valueTypesById, seenValueTypeIds);
921
875
  }
922
- function validateOutgoingTraverse(linkId, shape, path, ctx) {
923
- const targetTypeIds = [];
924
- for (const objectType of getObjectTypesForResult(shape, ctx)) {
925
- const link = objectType.links.find((candidate) => candidate.id === linkId);
926
- if (!link) {
927
- addIssue2(ctx, path, "unknown_link", `Outgoing traversal references unknown link '${linkId}' on '${objectType.id}'`);
928
- continue;
929
- }
930
- if (link.targetObjectTypeId === "*") {
931
- addIssue2(ctx, path, "wildcard_traverse_target", `Outgoing traversal over wildcard link '${objectType.id}.${linkId}' cannot infer a target object type`);
932
- continue;
933
- }
934
- const declaredTargets = Array.isArray(link.targetObjectTypeId) ? link.targetObjectTypeId : [link.targetObjectTypeId];
935
- for (const targetId of declaredTargets) {
936
- if (!ctx.ontology.getObjectTypeById(targetId)) {
937
- addIssue2(ctx, path, "unknown_link_target", `Outgoing traversal over '${objectType.id}.${linkId}' targets unknown object type '${targetId}'`);
938
- } else {
939
- targetTypeIds.push(targetId);
940
- }
941
- }
876
+ function isPrimaryExactPredicate(property, op) {
877
+ return property.primary === true && (op === "eq" || op === "in");
878
+ }
879
+ function keyForTypeIds(typeIds) {
880
+ return uniqueStrings(typeIds).sort().join("\x00");
881
+ }
882
+ function uniqueStrings(values) {
883
+ return [...new Set(values)];
884
+ }
885
+ function admitTransition(ctx, transition) {
886
+ if (transition.denial)
887
+ recordAdmissionDenial(ctx, transition.denial);
888
+ return transition.state;
889
+ }
890
+ function admitProperty(ctx, input) {
891
+ const denial = ctx.admission.property(input);
892
+ if (denial)
893
+ recordAdmissionDenial(ctx, denial);
894
+ }
895
+ function recordAdmissionDenial(ctx, denial) {
896
+ if (ctx.admissionDenials.length === 0)
897
+ ctx.admissionDenials.push(denial);
898
+ }
899
+ function addIssue(ctx, path, code, message) {
900
+ ctx.issues.push({ path, code, message });
901
+ }
902
+
903
+ // src/objects/vectors/resolve-query.ts
904
+ var QUERY_EMBEDDING_TIMEOUT_MS = 30000;
905
+ async function resolveVectorSearchText(query, ontology, models, signal) {
906
+ if (query.kind === "limit" || query.kind === "project") {
907
+ const input = await resolveVectorSearchText(query.input, ontology, models, signal);
908
+ return { ...query, input };
942
909
  }
943
- return { objectTypeIds: uniqueStrings(targetTypeIds) };
910
+ if (query.kind !== "vector" || typeof query.vector !== "string")
911
+ return query;
912
+ const model = resolveProfileEmbeddingModel(query, ontology, models);
913
+ const vector = await embedSearchText(query.vector, model, signal);
914
+ return { ...query, vector };
944
915
  }
945
- function validateIncomingTraverse(linkId, sourceObjectTypeId, shape, path, ctx) {
946
- let candidateTypes = ctx.ontology.listObjectTypes();
947
- if (sourceObjectTypeId !== undefined) {
948
- const sourceType = ctx.ontology.getObjectTypeById(sourceObjectTypeId);
949
- if (!sourceType) {
950
- addIssue2(ctx, path, "unknown_traverse_source", `Incoming traversal references unknown source object type '${sourceObjectTypeId}'`);
951
- return { objectTypeIds: [] };
952
- }
953
- candidateTypes = [sourceType];
916
+ function resolveProfileEmbeddingModel(query, ontology, models) {
917
+ const { objectTypeIds } = resolveObjectQueryResultShape2(query.input, {
918
+ ontology,
919
+ normalize: false
920
+ });
921
+ const objectTypeId = objectTypeIds.length === 1 ? objectTypeIds[0] : undefined;
922
+ if (!objectTypeId || !query.profile) {
923
+ throw new ObjectQueryExecutionError2("embedding_model_unavailable", "Vector search requires the embedding model registered for its profile.");
954
924
  }
955
- const sourceTypeIds = [];
956
- for (const sourceType of candidateTypes) {
957
- const link = sourceType.links.find((candidate) => candidate.id === linkId);
958
- if (!link)
959
- continue;
960
- if (link.targetObjectTypeId === "*") {
961
- sourceTypeIds.push(sourceType.id);
962
- continue;
963
- }
964
- const matches = shape.objectTypeIds.some((targetTypeId) => ctx.ontology.isValidLinkTarget(link.targetObjectTypeId, targetTypeId));
965
- if (matches)
966
- sourceTypeIds.push(sourceType.id);
925
+ const profile = ontology.resolveObjectType(objectTypeId).search?.vectors?.[query.profile];
926
+ if (!profile) {
927
+ throw new ObjectQueryExecutionError2("embedding_model_unavailable", "Vector search requires the embedding model registered for its profile.");
967
928
  }
968
- if (sourceTypeIds.length === 0) {
969
- addIssue2(ctx, path, "incoming_link_not_found", `Incoming traversal found no source object types with link '${linkId}' targeting ${shape.objectTypeIds.join(" | ")}`);
929
+ const model = models?.getByRef({
930
+ provider: profile.model.providerId,
931
+ modelId: profile.model.modelId
932
+ })?.model;
933
+ if (!model || !sameEmbeddingModel(model, profile.model)) {
934
+ throw new ObjectQueryExecutionError2("embedding_model_unavailable", "Vector search requires the embedding model registered for its profile.");
970
935
  }
971
- return { objectTypeIds: uniqueStrings(sourceTypeIds) };
972
- }
973
- function validateExpansions(expansions, shape, admissionState, path, ctx) {
974
- return expansions.map((expansion, index) => validateExpansion(expansion, shape, admissionState, `${path}[${index}]`, ctx));
936
+ return model;
975
937
  }
976
- function validateExpansion(expansion, shape, admissionState, path, ctx) {
977
- if (!expansion.linkId) {
978
- addIssue2(ctx, path, "missing_expand_link", "expand.linkId is required");
979
- return expansion;
938
+ async function embedSearchText(text, model, signal) {
939
+ const dimensions = model.definition.dimensions;
940
+ const timeout = AbortSignal.timeout(QUERY_EMBEDDING_TIMEOUT_MS);
941
+ const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
942
+ requestSignal.throwIfAborted();
943
+ const result = await model.embed({ texts: [text], purpose: "query", signal: requestSignal });
944
+ requestSignal.throwIfAborted();
945
+ if (!Array.isArray(result?.vectors) || result.vectors.length !== 1) {
946
+ throw new ObjectQueryExecutionError2("invalid_embedding_response", "Embedding model must return exactly one vector for the search text.");
980
947
  }
981
- const targetShape = expansion.direction === "incoming" ? resolveIncomingExpansionTargets(expansion.linkId, expansion.sourceObjectTypeId, shape, path, ctx) : resolveOutgoingExpansionTargets(expansion.linkId, expansion.sourceObjectTypeId, shape, path, ctx);
982
- const targetAdmissionState = admitTransition(ctx, ctx.admission.edge({
983
- state: admissionState,
984
- result: targetShape,
985
- linkId: expansion.linkId,
986
- direction: expansion.direction,
987
- sourceObjectTypeId: expansion.sourceObjectTypeId,
988
- use: "expand",
989
- path
990
- }));
991
- for (const targetTypeId of targetShape.objectTypeIds) {
992
- ctx.touchedObjectTypeIds.add(targetTypeId);
948
+ try {
949
+ return normalizeVector2(result.vectors[0], dimensions);
950
+ } catch {
951
+ throw new ObjectQueryExecutionError2("invalid_embedding_response", "Embedding model returned an invalid vector for the search profile.");
993
952
  }
994
- validateExpansionLimit(expansion.limit, path, ctx);
995
- const orderBy = expansion.orderBy ? validateSortFields(expansion.orderBy, targetShape, targetAdmissionState, "expand.orderBy", `${path}.orderBy`, ctx) : undefined;
996
- const expand = expansion.expand ? validateExpansions(expansion.expand, targetShape, targetAdmissionState, `${path}.expand`, ctx) : undefined;
997
- return { ...expansion, orderBy, expand };
998
953
  }
999
- function resolveOutgoingExpansionTargets(linkId, sourceObjectTypeId, shape, path, ctx) {
1000
- if (sourceObjectTypeId !== undefined) {
1001
- addIssue2(ctx, path, "expand_source_not_applicable", "expand.sourceObjectTypeId only applies to incoming expansion");
954
+
955
+ // src/objects/query/planner.ts
956
+ var DEFAULT_MAX_FALLBACK_ROWS = 1000;
957
+ var fallbackNodeKinds = new Set([
958
+ "start",
959
+ "refs",
960
+ "filter",
961
+ "sort",
962
+ "limit",
963
+ "page",
964
+ "project",
965
+ "expand"
966
+ ]);
967
+ function planObjectQuery2(query, options) {
968
+ const operation = options.operation ?? "queryObjects";
969
+ const providerIssues = collectProviderIssues(query, options.capabilities, {
970
+ operation,
971
+ hasImplementation: hasProviderImplementation(operation, options)
972
+ });
973
+ if (providerIssues.length === 0) {
974
+ return {
975
+ mode: "pushdown",
976
+ query,
977
+ providerIssues,
978
+ fallbackIssues: [],
979
+ issues: []
980
+ };
1002
981
  }
1003
- const targetTypeIds = [];
1004
- let foundOnAnyType = false;
1005
- for (const objectType of getObjectTypesForResult(shape, ctx)) {
1006
- const link = objectType.links.find((candidate) => candidate.id === linkId);
1007
- if (!link)
1008
- continue;
1009
- foundOnAnyType = true;
1010
- if (link.targetObjectTypeId === "*") {
1011
- addIssue2(ctx, path, "wildcard_expand_target", `Expansion over wildcard link '${objectType.id}.${linkId}' cannot infer a target object type`);
1012
- continue;
1013
- }
1014
- const declaredTargets = Array.isArray(link.targetObjectTypeId) ? link.targetObjectTypeId : [link.targetObjectTypeId];
1015
- for (const targetId of declaredTargets) {
1016
- if (!ctx.ontology.getObjectTypeById(targetId)) {
1017
- addIssue2(ctx, path, "unknown_expand_target", `Expansion over '${objectType.id}.${linkId}' targets unknown object type '${targetId}'`);
1018
- } else {
1019
- targetTypeIds.push(targetId);
982
+ const fallbackIssues = collectFallbackIssues(query, options);
983
+ const issues = [...providerIssues, ...fallbackIssues];
984
+ if (fallbackIssues.length === 0) {
985
+ return {
986
+ mode: "fallback",
987
+ query,
988
+ providerIssues,
989
+ fallbackIssues,
990
+ issues: [],
991
+ fallback: {
992
+ maxRows: options.maxFallbackRows ?? DEFAULT_MAX_FALLBACK_ROWS,
993
+ requiresExplicitBound: options.requiresExplicitFallbackBound !== false
1020
994
  }
1021
- }
1022
- }
1023
- if (!foundOnAnyType) {
1024
- addIssue2(ctx, path, "unknown_expand_link", `Expansion references link '${linkId}' not present on result type ${shape.objectTypeIds.join(" | ")} (an expand abandoned by a later traverse?)`);
995
+ };
1025
996
  }
1026
- return { objectTypeIds: uniqueStrings(targetTypeIds) };
997
+ return {
998
+ mode: "rejected",
999
+ query,
1000
+ providerIssues,
1001
+ fallbackIssues,
1002
+ issues
1003
+ };
1027
1004
  }
1028
- function resolveIncomingExpansionTargets(linkId, sourceObjectTypeId, shape, path, ctx) {
1029
- let candidateTypes = ctx.ontology.listObjectTypes();
1030
- if (sourceObjectTypeId !== undefined) {
1031
- const sourceType = ctx.ontology.getObjectTypeById(sourceObjectTypeId);
1032
- if (!sourceType) {
1033
- addIssue2(ctx, path, "unknown_expand_source", `Incoming expansion references unknown source object type '${sourceObjectTypeId}'`);
1034
- return { objectTypeIds: [] };
1035
- }
1036
- candidateTypes = [sourceType];
1037
- }
1038
- const sourceTypeIds = [];
1039
- for (const sourceType of candidateTypes) {
1040
- const link = sourceType.links.find((candidate) => candidate.id === linkId);
1041
- if (!link)
1042
- continue;
1043
- if (link.targetObjectTypeId === "*") {
1044
- sourceTypeIds.push(sourceType.id);
1045
- continue;
1046
- }
1047
- const matches = shape.objectTypeIds.some((targetTypeId) => ctx.ontology.isValidLinkTarget(link.targetObjectTypeId, targetTypeId));
1048
- if (matches)
1049
- sourceTypeIds.push(sourceType.id);
1005
+ function collectProviderIssues(query, capabilities, options) {
1006
+ const issues = [];
1007
+ collectOperationProviderIssues(capabilities, options, issues);
1008
+ collectNodeProviderIssues(query, "$", capabilities, issues);
1009
+ return issues;
1010
+ }
1011
+ function collectOperationProviderIssues(capabilities, options, issues) {
1012
+ const enabled = options.operation === "queryObjects" ? capabilities.queryObjects === true : options.operation === "countObjects" ? capabilities.countObjects === true : options.operation === "existsObjects" ? capabilities.existsObjects === true : capabilities.facetObjects === true;
1013
+ if (!enabled) {
1014
+ const code = `${operationIssuePrefix(options.operation)}_not_enabled`;
1015
+ addIssue2(issues, "$", code, `Object storage capabilities do not enable ${options.operation} pushdown`);
1050
1016
  }
1051
- if (sourceTypeIds.length === 0) {
1052
- addIssue2(ctx, path, "incoming_expand_link_not_found", `Incoming expansion found no source object types with link '${linkId}' targeting ${shape.objectTypeIds.join(" | ")}`);
1017
+ if (options.hasImplementation)
1018
+ return;
1019
+ const code = `${operationIssuePrefix(options.operation)}_not_implemented`;
1020
+ addIssue2(issues, "$", code, `Object storage does not implement ${options.operation}`);
1021
+ }
1022
+ function hasProviderImplementation(operation, options) {
1023
+ switch (operation) {
1024
+ case "queryObjects":
1025
+ return options.hasQueryObjects === true;
1026
+ case "countObjects":
1027
+ return options.hasCountObjects === true;
1028
+ case "existsObjects":
1029
+ return options.hasExistsObjects === true;
1030
+ case "facetObjects":
1031
+ return options.hasFacetObjects === true;
1053
1032
  }
1054
- return { objectTypeIds: uniqueStrings(sourceTypeIds) };
1055
1033
  }
1056
- function validateExpansionLimit(limit, path, ctx) {
1057
- if (limit === undefined)
1058
- return;
1059
- if (!Number.isInteger(limit) || limit < 0) {
1060
- addIssue2(ctx, `${path}.limit`, "invalid_expand_limit", "expand.limit must be a non-negative integer");
1034
+ function operationIssuePrefix(operation) {
1035
+ switch (operation) {
1036
+ case "queryObjects":
1037
+ return "query_objects";
1038
+ case "countObjects":
1039
+ return "count_objects";
1040
+ case "existsObjects":
1041
+ return "exists_objects";
1042
+ case "facetObjects":
1043
+ return "facet_objects";
1061
1044
  }
1062
1045
  }
1063
- function validateSet(inputs, op, path, ctx) {
1064
- if (inputs.length === 0) {
1065
- addIssue2(ctx, path, "empty_set", `Set operation '${op}' must include at least one input`);
1066
- return {
1067
- result: { objectTypeIds: [] },
1068
- query: { kind: "set", op, inputs },
1069
- admissionState: ctx.admission.empty()
1070
- };
1046
+ function collectNodeProviderIssues(query, path, capabilities, issues) {
1047
+ if (capabilities.nodes?.[query.kind] !== true) {
1048
+ addIssue2(issues, path, "query_node_not_supported", `Provider does not support query node '${query.kind}'`);
1049
+ }
1050
+ switch (query.kind) {
1051
+ case "start":
1052
+ if (query.includeSubtypes === true && capabilities.features?.includeSubtypes !== true) {
1053
+ addIssue2(issues, path, "include_subtypes_not_supported", "Provider does not support start.includeSubtypes expansion");
1054
+ }
1055
+ return;
1056
+ case "refs":
1057
+ return;
1058
+ case "filter":
1059
+ collectPredicateProviderIssues(query.predicate, `${path}.predicate`, capabilities, issues);
1060
+ collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
1061
+ return;
1062
+ case "text":
1063
+ case "vector":
1064
+ case "project":
1065
+ collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
1066
+ return;
1067
+ case "limit":
1068
+ collectProviderMaxLimitIssues(query.limit, path, capabilities, issues);
1069
+ collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
1070
+ return;
1071
+ case "page":
1072
+ collectProviderMaxPageSizeIssues(query.pageSize, path, capabilities, issues);
1073
+ collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
1074
+ return;
1075
+ case "traverse":
1076
+ if (capabilities.traversalDirections?.[query.direction] !== true) {
1077
+ addIssue2(issues, path, "traversal_direction_not_supported", `Provider does not support ${query.direction} traversal`);
1078
+ }
1079
+ collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
1080
+ return;
1081
+ case "set":
1082
+ if (capabilities.setOps?.[query.op] !== true) {
1083
+ addIssue2(issues, path, "set_operation_not_supported", `Provider does not support set operation '${query.op}'`);
1084
+ }
1085
+ query.inputs.forEach((input, index) => {
1086
+ collectNodeProviderIssues(input, `${path}.inputs[${index}]`, capabilities, issues);
1087
+ });
1088
+ return;
1089
+ case "sort":
1090
+ query.fields.forEach((field, index) => {
1091
+ collectSortProviderIssues(field, `${path}.fields[${index}]`, capabilities, issues);
1092
+ });
1093
+ collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
1094
+ return;
1095
+ case "expand":
1096
+ query.expansions.forEach((expansion, index) => {
1097
+ collectExpansionProviderIssues(expansion, `${path}.expansions[${index}]`, capabilities, issues);
1098
+ });
1099
+ collectNodeProviderIssues(query.input, `${path}.input`, capabilities, issues);
1100
+ return;
1071
1101
  }
1072
- const results = inputs.map((input, index) => validateQueryNode(input, `${path}.inputs[${index}]`, ctx));
1073
- const first = results[0]?.result ?? { objectTypeIds: [] };
1074
- const expected = keyForTypeIds(first.objectTypeIds);
1075
- results.forEach((result, index) => {
1076
- if (keyForTypeIds(result.result.objectTypeIds) !== expected) {
1077
- addIssue2(ctx, `${path}.inputs[${index}]`, "incompatible_set_input", `Set operation '${op}' inputs must produce the same object type set`);
1078
- }
1079
- });
1080
- return {
1081
- result: first,
1082
- query: { kind: "set", op, inputs: results.map((result) => result.query) },
1083
- admissionState: ctx.admission.set({
1084
- op,
1085
- states: results.map((result) => result.admissionState),
1086
- path
1087
- })
1088
- };
1089
1102
  }
1090
- function validateSortFields(fields, shape, admissionState, use, path, ctx) {
1091
- if (fields.length === 0) {
1092
- addIssue2(ctx, path, "empty_sort", "Sort must include at least one field");
1093
- return [];
1103
+ function collectExpansionProviderIssues(expansion, path, capabilities, issues) {
1104
+ if (expansion.cardinality === undefined) {
1105
+ addIssue2(issues, path, "expand_cardinality_unresolved", `Provider cannot push down expansion '${expansion.linkId}' without a resolved cardinality`);
1094
1106
  }
1095
- const seen = new Set;
1096
- return fields.map((field, index) => {
1097
- if (field.direction && field.direction !== "asc" && field.direction !== "desc") {
1098
- addIssue2(ctx, `${path}.fields[${index}]`, "invalid_sort_direction", "Sort direction must be asc or desc");
1099
- }
1100
- const key = field.kind === "property" ? `property:${field.propertyId}` : "relevance";
1101
- if (seen.has(key)) {
1102
- addIssue2(ctx, `${path}.fields[${index}]`, "duplicate_sort_field", `Duplicate sort field '${key}'`);
1103
- }
1104
- seen.add(key);
1105
- if (field.kind === "relevance")
1106
- return field;
1107
- admitProperty(ctx, {
1108
- state: admissionState,
1109
- propertyId: field.propertyId,
1110
- use,
1111
- path: `${path}.fields[${index}]`
1112
- });
1113
- const { scalarKind: _authoredScalarKind, ...authoredField } = field;
1114
- const schemas = [];
1115
- for (const { objectType, property } of getPropertiesForResult(field.propertyId, shape, `${path}.fields[${index}]`, ctx)) {
1116
- if (property.query?.searchable !== true || property.query.sortable !== true) {
1117
- addIssue2(ctx, `${path}.fields[${index}]`, "property_not_sortable", `Property '${property.id}' on '${objectType.id}' must set query.searchable: true and query.sortable: true before it can be sorted`);
1118
- continue;
1119
- }
1120
- const schema = resolveSchemaForProperty(property, objectType.id, ctx, `${path}.fields[${index}]`);
1121
- if (schema)
1122
- schemas.push(schema);
1123
- if (schema && !isSortableSchema(schema)) {
1124
- addIssue2(ctx, `${path}.fields[${index}]`, "sort_field_not_orderable", `Sort field '${property.id}' on '${objectType.id}' is not orderable`);
1125
- }
1126
- }
1127
- const scalarKind = resolveCommonQueryScalarKind(schemas, field.propertyId, `${path}.fields[${index}]`, ctx);
1128
- return { ...authoredField, ...scalarKind ? { scalarKind } : {} };
1107
+ expansion.orderBy?.forEach((field, index) => {
1108
+ collectSortProviderIssues(field, `${path}.orderBy[${index}]`, capabilities, issues);
1109
+ });
1110
+ expansion.expand?.forEach((nested, index) => {
1111
+ collectExpansionProviderIssues(nested, `${path}.expand[${index}]`, capabilities, issues);
1129
1112
  });
1130
1113
  }
1131
- function validateLimit(limit, path, ctx) {
1132
- if (!Number.isInteger(limit) || limit < 0) {
1133
- addIssue2(ctx, path, "invalid_limit", "limit must be a non-negative integer");
1134
- } else if (limit > ctx.maxLimit) {
1135
- addIssue2(ctx, path, "limit_too_large", `limit must be <= ${ctx.maxLimit}`);
1114
+ function collectPredicateProviderIssues(predicate, path, capabilities, issues) {
1115
+ const scalarOperation = predicateScalarOperation(predicate);
1116
+ if ("scalarKind" in predicate && predicate.scalarKind !== undefined && scalarOperation !== undefined && capabilities.scalarOperations?.[predicate.scalarKind]?.[scalarOperation] !== true) {
1117
+ addIssue2(issues, path, "scalar_operation_not_supported", `Provider does not support ${scalarOperation} for '${predicate.scalarKind}' values`);
1136
1118
  }
1137
- }
1138
- function validatePage(pageSize, pageToken, path, ctx) {
1139
- if (!Number.isInteger(pageSize) || pageSize <= 0) {
1140
- addIssue2(ctx, path, "invalid_page_size", "pageSize must be a positive integer");
1141
- } else if (pageSize > ctx.maxPageSize) {
1142
- addIssue2(ctx, path, "page_size_too_large", `pageSize must be <= ${ctx.maxPageSize}`);
1119
+ if (capabilities.predicateOps?.[predicate.op] !== true) {
1120
+ addIssue2(issues, path, "predicate_operation_not_supported", `Provider does not support predicate operation '${predicate.op}'`);
1143
1121
  }
1144
- if (pageToken !== undefined && pageToken.length === 0) {
1145
- addIssue2(ctx, path, "empty_page_token", "pageToken must not be empty when provided");
1122
+ switch (predicate.op) {
1123
+ case "and":
1124
+ case "or":
1125
+ predicate.items.forEach((item, index) => {
1126
+ collectPredicateProviderIssues(item, `${path}.items[${index}]`, capabilities, issues);
1127
+ });
1128
+ return;
1129
+ case "not":
1130
+ collectPredicateProviderIssues(predicate.item, `${path}.item`, capabilities, issues);
1131
+ return;
1132
+ default:
1133
+ return;
1146
1134
  }
1147
1135
  }
1148
- function validateProjection(properties, shape, admissionState, path, ctx) {
1149
- if (!properties)
1150
- return;
1151
- if (properties.length === 0) {
1152
- addIssue2(ctx, path, "empty_projection", "project.properties must not be empty when provided");
1136
+ function predicateScalarOperation(predicate) {
1137
+ switch (predicate.op) {
1138
+ case "eq":
1139
+ case "neq":
1140
+ case "in":
1141
+ case "contains":
1142
+ return "equality";
1143
+ case "lt":
1144
+ case "lte":
1145
+ case "gt":
1146
+ case "gte":
1147
+ return "ordering";
1148
+ default:
1149
+ return;
1153
1150
  }
1154
- const seen = new Set;
1155
- properties.forEach((propertyId, index) => {
1156
- admitProperty(ctx, {
1157
- state: admissionState,
1158
- propertyId,
1159
- use: "project",
1160
- path: `${path}.properties[${index}]`
1161
- });
1162
- if (seen.has(propertyId)) {
1163
- addIssue2(ctx, `${path}.properties[${index}]`, "duplicate_projection_property", `Duplicate projected property '${propertyId}'`);
1164
- }
1165
- seen.add(propertyId);
1166
- getPropertiesForResult(propertyId, shape, `${path}.properties[${index}]`, ctx);
1167
- });
1168
1151
  }
1169
- function getObjectTypesForResult(shape, ctx) {
1170
- const objectTypes = [];
1171
- for (const objectTypeId of shape.objectTypeIds) {
1172
- const objectType = ctx.ontology.getObjectTypeById(objectTypeId);
1173
- if (objectType)
1174
- objectTypes.push(objectType);
1152
+ function collectSortProviderIssues(field, path, capabilities, issues) {
1153
+ if (field.kind === "property" && field.scalarKind !== undefined && capabilities.scalarOperations?.[field.scalarKind]?.ordering !== true) {
1154
+ addIssue2(issues, path, "scalar_operation_not_supported", `Provider does not support ordering for '${field.scalarKind}' values`);
1175
1155
  }
1176
- return objectTypes;
1156
+ if (capabilities.sortKinds?.[field.kind] === true)
1157
+ return;
1158
+ addIssue2(issues, path, "sort_kind_not_supported", `Provider does not support sort kind '${field.kind}'`);
1177
1159
  }
1178
- function getPropertiesForResult(propertyId, shape, path, ctx, options = {}) {
1179
- const results = [];
1180
- for (const objectType of getObjectTypesForResult(shape, ctx)) {
1181
- const property = getProperty(objectType, propertyId);
1182
- if (!property) {
1183
- if (options.reportMissing === false) {
1184
- continue;
1185
- }
1186
- addIssue2(ctx, path, "unknown_property", `Property '${propertyId}' does not exist on object type '${objectType.id}'`);
1187
- continue;
1188
- }
1189
- results.push({ objectType, property });
1160
+ function collectProviderMaxLimitIssues(limit, path, capabilities, issues) {
1161
+ const maxLimit = capabilities.limits?.maxLimit;
1162
+ if (maxLimit === undefined)
1163
+ return;
1164
+ if (!Number.isInteger(maxLimit) || maxLimit < 0) {
1165
+ addIssue2(issues, path, "invalid_provider_limit_capability", "Provider limit capability maxLimit must be a non-negative integer");
1166
+ return;
1190
1167
  }
1191
- return results;
1192
- }
1193
- function getProperty(objectType, propertyId) {
1194
- return objectType.properties.find((property) => property.id === propertyId);
1195
- }
1196
- function assertStaticSearchProperty(objectTypeId, property, context, path, ctx) {
1197
- if (property.mode === "telemetry") {
1198
- addIssue2(ctx, path, "telemetry_search_property", `${context} references telemetry property '${property.id}' on '${objectTypeId}'. Search can only reference static properties because telemetry latest values are not object-query indexed.`);
1168
+ if (limit > maxLimit) {
1169
+ addIssue2(issues, path, "provider_limit_too_large", `Provider supports limit up to ${maxLimit}`);
1199
1170
  }
1200
1171
  }
1201
- function assertQueryFlag(objectTypeId, property, flag, context, path, ctx) {
1202
- if (property.query?.searchable === true && property.query[flag] === true)
1172
+ function collectProviderMaxPageSizeIssues(pageSize, path, capabilities, issues) {
1173
+ const maxPageSize = capabilities.limits?.maxPageSize;
1174
+ if (maxPageSize === undefined)
1175
+ return;
1176
+ if (!Number.isInteger(maxPageSize) || maxPageSize <= 0) {
1177
+ addIssue2(issues, path, "invalid_provider_page_size_capability", "Provider limit capability maxPageSize must be a positive integer");
1203
1178
  return;
1204
- addIssue2(ctx, path, "query_field_not_enabled", `${context} field '${property.id}' on '${objectTypeId}' must set query.searchable: true and query.${flag}: true`);
1205
- }
1206
- function resolveSchemaForProperty(property, objectTypeId, ctx, path) {
1207
- try {
1208
- return resolveSchema(property.schema, ctx.valueTypesById);
1209
- } catch (error) {
1210
- addIssue2(ctx, path, "unresolved_property_schema", error instanceof Error ? error.message : `Could not resolve schema for '${objectTypeId}.${property.id}'`);
1211
- return null;
1179
+ }
1180
+ if (pageSize > maxPageSize) {
1181
+ addIssue2(issues, path, "provider_page_size_too_large", `Provider supports pageSize up to ${maxPageSize}`);
1212
1182
  }
1213
1183
  }
1214
- function resolveSchema(schema, valueTypesById, seenValueTypeIds = new Set) {
1215
- if (typeof schema === "string")
1216
- return schema;
1217
- if (schema.type !== "valueTypeRef")
1218
- return schema;
1219
- if (seenValueTypeIds.has(schema.valueTypeId)) {
1220
- throw new Error(`Circular valueTypeRef '${schema.valueTypeId}'`);
1184
+ function collectFallbackIssues(query, options) {
1185
+ const issues = [];
1186
+ if (options.allowFallback === false) {
1187
+ addIssue2(issues, "$", "fallback_disabled", "Object query fallback is disabled");
1188
+ return issues;
1221
1189
  }
1222
- const resolved = schema._resolved ?? valueTypesById.get(schema.valueTypeId)?.schema;
1223
- if (!resolved) {
1224
- throw new Error(`Unknown valueTypeRef '${schema.valueTypeId}'`);
1190
+ const maxFallbackRows = options.maxFallbackRows ?? DEFAULT_MAX_FALLBACK_ROWS;
1191
+ if (!Number.isInteger(maxFallbackRows) || maxFallbackRows <= 0) {
1192
+ addIssue2(issues, "$", "invalid_fallback_bound", "maxFallbackRows must be a positive integer");
1225
1193
  }
1226
- seenValueTypeIds.add(schema.valueTypeId);
1227
- return resolveSchema(resolved, valueTypesById, seenValueTypeIds);
1228
- }
1229
- function isPrimaryExactPredicate(property, op) {
1230
- return property.primary === true && (op === "eq" || op === "in");
1231
- }
1232
- function keyForTypeIds(typeIds) {
1233
- return uniqueStrings(typeIds).sort().join("\x00");
1234
- }
1235
- function uniqueStrings(values) {
1236
- return [...new Set(values)];
1237
- }
1238
- function admitTransition(ctx, transition) {
1239
- if (transition.denial)
1240
- recordAdmissionDenial(ctx, transition.denial);
1241
- return transition.state;
1194
+ if (options.requiresExplicitFallbackBound !== false && !hasExplicitResultBound(query)) {
1195
+ addIssue2(issues, "$", "fallback_requires_bound", "Fallback requires an explicit limit or page node");
1196
+ }
1197
+ collectFallbackNodeIssues(query, "$", issues);
1198
+ return issues;
1242
1199
  }
1243
- function admitProperty(ctx, input) {
1244
- const denial = ctx.admission.property(input);
1245
- if (denial)
1246
- recordAdmissionDenial(ctx, denial);
1200
+ function collectFallbackNodeIssues(query, path, issues) {
1201
+ if (!fallbackNodeKinds.has(query.kind)) {
1202
+ addIssue2(issues, path, "fallback_node_not_supported", `Fallback does not support query node '${query.kind}'`);
1203
+ }
1204
+ switch (query.kind) {
1205
+ case "start":
1206
+ case "refs":
1207
+ return;
1208
+ case "filter":
1209
+ case "limit":
1210
+ case "page":
1211
+ case "project":
1212
+ case "expand":
1213
+ collectFallbackNodeIssues(query.input, `${path}.input`, issues);
1214
+ return;
1215
+ case "sort":
1216
+ query.fields.forEach((field, index) => {
1217
+ if (field.kind === "relevance") {
1218
+ addIssue2(issues, `${path}.fields[${index}]`, "fallback_sort_kind_not_supported", "Fallback does not support relevance sorting");
1219
+ }
1220
+ });
1221
+ collectFallbackNodeIssues(query.input, `${path}.input`, issues);
1222
+ return;
1223
+ case "text":
1224
+ case "vector":
1225
+ case "traverse":
1226
+ collectFallbackNodeIssues(query.input, `${path}.input`, issues);
1227
+ return;
1228
+ case "set":
1229
+ query.inputs.forEach((input, index) => {
1230
+ collectFallbackNodeIssues(input, `${path}.inputs[${index}]`, issues);
1231
+ });
1232
+ return;
1233
+ }
1247
1234
  }
1248
- function recordAdmissionDenial(ctx, denial) {
1249
- if (ctx.admissionDenials.length === 0)
1250
- ctx.admissionDenials.push(denial);
1235
+ function hasExplicitResultBound(query) {
1236
+ switch (query.kind) {
1237
+ case "limit":
1238
+ case "page":
1239
+ case "refs":
1240
+ return true;
1241
+ case "start":
1242
+ return false;
1243
+ case "set":
1244
+ return query.inputs.some(hasExplicitResultBound);
1245
+ default:
1246
+ return hasExplicitResultBound(query.input);
1247
+ }
1251
1248
  }
1252
- function addIssue2(ctx, path, code, message) {
1253
- ctx.issues.push({ path, code, message });
1249
+ function addIssue2(issues, path, code, message) {
1250
+ issues.push({ path, code, message });
1254
1251
  }
1255
1252
 
1256
1253
  // src/objects/query/executor.ts
@@ -2460,6 +2457,6 @@ async function hydrateLinkQueryObjects(projectId, selected, links, storage) {
2460
2457
  return row ? [row] : [];
2461
2458
  });
2462
2459
  }
2463
- export { planObjectQuery2, validateObjectQuery2, validateObjectQueryWithAdmission, collectObjectQueryValidationIssues2, resolveObjectQueryResultShape2, executeObjectQuery2, countObjects2, existsObjects2, facetObjects2, validateObjectFacetRequests, explainObjectQuery2, preflightObjectQueryLinks, executeObjectQueryLinks2 };
2460
+ export { validateObjectQuery2, validateObjectQueryWithAdmission, collectObjectQueryValidationIssues2, resolveObjectQueryResultShape2, planObjectQuery2, executeObjectQuery2, countObjects2, existsObjects2, facetObjects2, validateObjectFacetRequests, explainObjectQuery2, preflightObjectQueryLinks, executeObjectQueryLinks2 };
2464
2461
 
2465
- //# debugId=E8797FF7D0E5083164756E2164756E21
2462
+ //# debugId=96E245CAF140894464756E2164756E21