@superpanel-sdk/runtime 0.2.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/README.md +30 -10
  2. package/dist/builder/codegen/emit.d.ts +25 -12
  3. package/dist/builder/codegen/emit.js +339 -78
  4. package/dist/builder/codegen/generate.d.ts +21 -2
  5. package/dist/builder/codegen/generate.js +147 -34
  6. package/dist/builder/company-import-graph.d.ts +2 -0
  7. package/dist/builder/company-import-graph.js +40 -8
  8. package/dist/builder/company-imports.d.ts +29 -0
  9. package/dist/builder/company-imports.js +122 -0
  10. package/dist/builder/compile/compile-sequence.d.ts +61 -8
  11. package/dist/builder/compile/compile-sequence.js +334 -80
  12. package/dist/builder/compile/function-code.d.ts +19 -3
  13. package/dist/builder/compile/function-code.js +123 -45
  14. package/dist/builder/compile/registry.d.ts +7 -3
  15. package/dist/builder/compile/registry.js +30 -27
  16. package/dist/builder/errors.d.ts +9 -0
  17. package/dist/builder/errors.js +17 -0
  18. package/dist/builder/function-params.d.ts +10 -0
  19. package/dist/builder/function-params.js +203 -0
  20. package/dist/builder/load.d.ts +70 -10
  21. package/dist/builder/load.js +258 -67
  22. package/dist/builder/paths.d.ts +9 -1
  23. package/dist/builder/paths.js +32 -2
  24. package/dist/builder/schedules.d.ts +25 -0
  25. package/dist/builder/schedules.js +101 -0
  26. package/dist/builder/slot-schema-validation.d.ts +4 -0
  27. package/dist/builder/slot-schema-validation.js +10 -0
  28. package/dist/builder/typecheck.d.ts +8 -0
  29. package/dist/builder/typecheck.js +102 -0
  30. package/dist/builder/types.d.ts +127 -19
  31. package/dist/builder/types.js +21 -1
  32. package/dist/cli/auth/auth.d.ts +37 -0
  33. package/dist/cli/auth/auth.js +211 -0
  34. package/dist/cli/auth/backend-url.d.ts +5 -0
  35. package/dist/cli/auth/backend-url.js +25 -0
  36. package/dist/cli/auth/clipboard.d.ts +9 -0
  37. package/dist/cli/auth/clipboard.js +38 -0
  38. package/dist/cli/auth/http.d.ts +20 -0
  39. package/dist/cli/auth/http.js +27 -0
  40. package/dist/cli/auth/index.d.ts +10 -0
  41. package/dist/cli/auth/index.js +27 -0
  42. package/dist/cli/auth/progress.d.ts +12 -0
  43. package/dist/cli/auth/progress.js +15 -0
  44. package/dist/cli/auth/serve-login.d.ts +8 -0
  45. package/dist/cli/auth/serve-login.js +24 -0
  46. package/dist/cli/calls/index.d.ts +177 -0
  47. package/dist/cli/calls/index.js +96 -0
  48. package/dist/cli/compile/index.js +3 -0
  49. package/dist/cli/config.d.ts +10 -0
  50. package/dist/cli/config.js +52 -0
  51. package/dist/cli/debug/index.d.ts +31 -0
  52. package/dist/cli/debug/index.js +119 -0
  53. package/dist/cli/exec/index.d.ts +39 -0
  54. package/dist/cli/exec/index.js +138 -0
  55. package/dist/cli/exec/session.d.ts +48 -0
  56. package/dist/cli/exec/session.js +82 -0
  57. package/dist/cli/exec/trace.d.ts +18 -0
  58. package/dist/cli/exec/trace.js +52 -0
  59. package/dist/cli/index.js +112 -9
  60. package/dist/cli/logs/index.d.ts +47 -0
  61. package/dist/cli/logs/index.js +113 -0
  62. package/dist/cli/mobile/index.d.ts +12 -0
  63. package/dist/cli/mobile/index.js +405 -0
  64. package/dist/cli/serve/index.d.ts +109 -3
  65. package/dist/cli/serve/index.js +506 -118
  66. package/dist/cli/turns/index.d.ts +105 -0
  67. package/dist/cli/turns/index.js +128 -0
  68. package/dist/cli/turns/run.d.ts +13 -0
  69. package/dist/cli/turns/run.js +57 -0
  70. package/dist/cli/turns/shadow.d.ts +112 -0
  71. package/dist/cli/turns/shadow.js +54 -0
  72. package/dist/cli/type-gen/index.js +9 -9
  73. package/dist/compiler.d.ts +7 -0
  74. package/dist/compiler.js +7 -0
  75. package/dist/compute-host/compute-host.d.ts +14 -23
  76. package/dist/compute-host/compute-host.js +18 -30
  77. package/dist/compute-host/index.d.ts +2 -4
  78. package/dist/compute-host/index.js +1 -2
  79. package/dist/compute-host/source-compute-host.d.ts +10 -10
  80. package/dist/compute-host/source-compute-host.js +33 -40
  81. package/dist/core/case-orchestrator/contact-hours.d.ts +70 -0
  82. package/dist/core/case-orchestrator/contact-hours.js +127 -0
  83. package/dist/core/case-orchestrator/index.d.ts +3 -0
  84. package/dist/core/case-orchestrator/index.js +3 -0
  85. package/dist/core/case-orchestrator/render.d.ts +75 -0
  86. package/dist/core/case-orchestrator/render.js +244 -0
  87. package/dist/core/case-orchestrator/time.d.ts +11 -0
  88. package/dist/core/case-orchestrator/time.js +29 -0
  89. package/dist/core/case-orchestrator/types.d.ts +708 -0
  90. package/dist/core/case-orchestrator/types.js +42 -0
  91. package/dist/core/context/call.d.ts +291 -0
  92. package/dist/core/context/call.js +94 -0
  93. package/dist/core/context/channels.d.ts +60 -0
  94. package/dist/core/context/channels.js +35 -0
  95. package/dist/core/context/contact.d.ts +96 -6
  96. package/dist/core/context/contact.js +48 -2
  97. package/dist/core/context/email-message.d.ts +198 -0
  98. package/dist/core/context/email-message.js +86 -0
  99. package/dist/core/context/email.d.ts +109 -0
  100. package/dist/core/context/email.js +90 -0
  101. package/dist/core/context/exchanges.d.ts +167 -0
  102. package/dist/core/context/exchanges.js +81 -0
  103. package/dist/core/context/instances.d.ts +307 -0
  104. package/dist/core/context/instances.js +137 -0
  105. package/dist/core/context/kv.d.ts +113 -0
  106. package/dist/core/context/kv.js +60 -0
  107. package/dist/core/context/platform-registry.d.ts +544 -4
  108. package/dist/core/context/platform-registry.js +14 -2
  109. package/dist/core/context/platform.d.ts +2 -1
  110. package/dist/core/context/sms.d.ts +177 -0
  111. package/dist/core/context/sms.js +77 -0
  112. package/dist/core/context/types.d.ts +79 -48
  113. package/dist/core/context/types.js +1 -7
  114. package/dist/core/conversation/index.d.ts +85 -0
  115. package/dist/core/conversation/index.js +133 -0
  116. package/dist/core/functions/errors.d.ts +74 -0
  117. package/dist/core/functions/errors.js +26 -0
  118. package/dist/core/functions/events.d.ts +192 -0
  119. package/dist/core/functions/events.js +23 -0
  120. package/dist/core/functions/schedules.d.ts +50 -0
  121. package/dist/core/functions/schedules.js +20 -0
  122. package/dist/core/functions/types.d.ts +29 -73
  123. package/dist/core/functions/types.js +1 -4
  124. package/dist/core/office-hours/index.d.ts +39 -0
  125. package/dist/core/office-hours/index.js +187 -0
  126. package/dist/core/page/define.d.ts +904 -0
  127. package/dist/core/page/define.js +518 -0
  128. package/dist/core/page/envelope.d.ts +55 -0
  129. package/dist/core/page/envelope.js +4 -0
  130. package/dist/core/page/fns.d.ts +448 -0
  131. package/dist/core/page/fns.js +539 -0
  132. package/dist/core/page/memory-reader.d.ts +27 -0
  133. package/dist/core/page/memory-reader.js +299 -0
  134. package/dist/core/page/ops.d.ts +58 -0
  135. package/dist/core/page/ops.js +220 -0
  136. package/dist/core/page/page-host.d.ts +20 -0
  137. package/dist/core/page/page-host.js +57 -0
  138. package/dist/core/page/records-query.d.ts +234 -0
  139. package/dist/core/page/records-query.js +780 -0
  140. package/dist/core/page/render.d.ts +295 -0
  141. package/dist/core/page/render.js +1060 -0
  142. package/dist/core/page/sandbox.d.ts +85 -0
  143. package/dist/core/page/sandbox.js +357 -0
  144. package/dist/core/page/time-format.d.ts +46 -0
  145. package/dist/core/page/time-format.js +125 -0
  146. package/dist/core/page/timeline.d.ts +40 -0
  147. package/dist/core/page/timeline.js +2 -0
  148. package/dist/core/page/typed.d.ts +648 -0
  149. package/dist/core/page/typed.js +2 -0
  150. package/dist/core/page/workspace.d.ts +57 -0
  151. package/dist/core/page/workspace.js +266 -0
  152. package/dist/core/party/index.d.ts +3 -0
  153. package/dist/core/party/index.js +3 -0
  154. package/dist/core/party/schema.d.ts +208 -0
  155. package/dist/core/party/schema.js +113 -0
  156. package/dist/core/party/types.d.ts +35 -0
  157. package/dist/core/party/types.js +15 -0
  158. package/dist/core/sequence/define.d.ts +56 -0
  159. package/dist/core/sequence/define.js +10 -0
  160. package/dist/core/sequence/index.d.ts +63 -0
  161. package/dist/core/sequence/index.js +231 -0
  162. package/dist/core/slot/binding.d.ts +40 -0
  163. package/dist/core/slot/binding.js +2 -0
  164. package/dist/core/slot/company-metadata.d.ts +102 -0
  165. package/dist/core/slot/company-metadata.js +536 -0
  166. package/dist/core/slot/compile-schema.d.ts +19 -13
  167. package/dist/core/slot/compile-schema.js +51 -248
  168. package/dist/core/slot/contact-timezone.d.ts +110 -0
  169. package/dist/core/slot/contact-timezone.js +186 -0
  170. package/dist/core/slot/field.d.ts +2 -25
  171. package/dist/core/slot/field.js +0 -71
  172. package/dist/core/slot/id-ref.d.ts +17 -2
  173. package/dist/core/slot/id-ref.js +16 -8
  174. package/dist/core/slot/index.d.ts +12 -13
  175. package/dist/core/slot/index.js +11 -12
  176. package/dist/core/slot/naming.d.ts +15 -0
  177. package/dist/core/slot/naming.js +96 -0
  178. package/dist/core/slot/platform-fields.d.ts +119 -0
  179. package/dist/core/slot/platform-fields.js +121 -0
  180. package/dist/core/slot/ref.d.ts +16 -0
  181. package/dist/core/slot/ref.js +14 -0
  182. package/dist/core/slot/row.d.ts +2 -9
  183. package/dist/core/slot/row.js +13 -23
  184. package/dist/core/slot/schema-compat.d.ts +17 -0
  185. package/dist/core/slot/schema-compat.js +103 -0
  186. package/dist/core/slot/schema.d.ts +353 -76
  187. package/dist/core/slot/schema.js +163 -10
  188. package/dist/core/slot/types.d.ts +30 -103
  189. package/dist/core/slot/value.d.ts +28 -3
  190. package/dist/core/slot/value.js +93 -57
  191. package/dist/dev-overlay/protocol.d.ts +74 -8
  192. package/dist/fn-context/build-context.d.ts +3 -1
  193. package/dist/fn-context/build-context.js +26 -10
  194. package/dist/fn-context/fake/empty-api.js +9 -28
  195. package/dist/fn-context/fake/index.d.ts +13 -4
  196. package/dist/fn-context/fake/index.js +29 -2
  197. package/dist/fn-context/fn-params.d.ts +3 -0
  198. package/dist/fn-context/fn-params.js +10 -0
  199. package/dist/fn-context/index.d.ts +5 -5
  200. package/dist/fn-context/index.js +4 -4
  201. package/dist/fn-context/log.js +2 -2
  202. package/dist/fn-context/make-ctx.d.ts +11 -0
  203. package/dist/fn-context/make-ctx.js +20 -9
  204. package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +32 -0
  205. package/dist/fn-context/rpc/case-orchestrator-tools.js +12 -0
  206. package/dist/fn-context/rpc/errors.d.ts +13 -0
  207. package/dist/fn-context/rpc/errors.js +22 -0
  208. package/dist/fn-context/rpc/index.d.ts +4 -2
  209. package/dist/fn-context/rpc/index.js +4 -2
  210. package/dist/fn-context/rpc/integration/index.d.ts +15 -5
  211. package/dist/fn-context/rpc/integration/index.js +1 -1
  212. package/dist/fn-context/rpc/integration/list-surfaces.d.ts +21 -0
  213. package/dist/fn-context/rpc/integration/{list-bindings.js → list-surfaces.js} +4 -4
  214. package/dist/fn-context/rpc/log.d.ts +11 -1
  215. package/dist/fn-context/rpc/platform.d.ts +13 -3
  216. package/dist/fn-context/rpc/platform.js +1 -1
  217. package/dist/fn-context/rpc/records.d.ts +85 -0
  218. package/dist/fn-context/rpc/records.js +24 -0
  219. package/dist/fn-context/rpc/trpc-client.d.ts +44 -2
  220. package/dist/fn-context/rpc/trpc-client.js +222 -12
  221. package/dist/fn-context/rpc/trpc-init.d.ts +21 -3
  222. package/dist/fn-context/rpc/trpc-init.js +10 -4
  223. package/dist/fn-context/rpc/trpc-router-factory.d.ts +111 -7
  224. package/dist/fn-context/rpc/trpc-router-factory.js +2 -0
  225. package/dist/fn-context/run-scope.d.ts +4 -0
  226. package/dist/fn-context/run-scope.js +9 -0
  227. package/dist/fn-context/types.d.ts +52 -5
  228. package/dist/index.d.ts +36 -46
  229. package/dist/index.js +30 -33
  230. package/dist/logger.d.ts +11 -0
  231. package/dist/logger.js +49 -0
  232. package/dist/platform.d.ts +75 -0
  233. package/dist/platform.js +57 -0
  234. package/dist/testing-compile-child.d.ts +2 -0
  235. package/dist/testing-compile-child.js +20 -0
  236. package/dist/testing.d.ts +368 -0
  237. package/dist/testing.js +439 -0
  238. package/dist/voice-service/actions.d.ts +50 -6
  239. package/dist/voice-service/index.d.ts +2 -0
  240. package/dist/voice-service/index.js +2 -0
  241. package/dist/voice-service/protocol.d.ts +604 -0
  242. package/dist/voice-service/protocol.js +67 -0
  243. package/dist/voice-service/types.d.ts +55 -8
  244. package/dist/worker.d.ts +11 -0
  245. package/dist/worker.js +11 -0
  246. package/package.json +38 -15
  247. package/dist/builder/compile/compile-agenda.d.ts +0 -15
  248. package/dist/builder/compile/compile-agenda.js +0 -64
  249. package/dist/builder/merge.d.ts +0 -4
  250. package/dist/builder/merge.js +0 -19
  251. package/dist/compute-host/read-only-cache.d.ts +0 -20
  252. package/dist/compute-host/read-only-cache.js +0 -61
  253. package/dist/core/context/db.d.ts +0 -69
  254. package/dist/core/context/db.js +0 -44
  255. package/dist/core/context/docusign.d.ts +0 -134
  256. package/dist/core/context/docusign.js +0 -108
  257. package/dist/core/context/filevine.d.ts +0 -323
  258. package/dist/core/context/filevine.js +0 -254
  259. package/dist/core/context/http.d.ts +0 -9
  260. package/dist/core/context/http.js +0 -4
  261. package/dist/core/context/hubspot.d.ts +0 -250
  262. package/dist/core/context/hubspot.js +0 -191
  263. package/dist/core/context/twilio.d.ts +0 -50
  264. package/dist/core/context/twilio.js +0 -42
  265. package/dist/core/functions/define/case-context.d.ts +0 -14
  266. package/dist/core/functions/define/case-context.js +0 -15
  267. package/dist/core/functions/define/compute.d.ts +0 -7
  268. package/dist/core/functions/define/compute.js +0 -17
  269. package/dist/core/functions/define/custom.d.ts +0 -4
  270. package/dist/core/functions/define/custom.js +0 -8
  271. package/dist/core/functions/define/system.d.ts +0 -7
  272. package/dist/core/functions/define/system.js +0 -12
  273. package/dist/core/functions/func-ref.d.ts +0 -16
  274. package/dist/core/functions/func-ref.js +0 -20
  275. package/dist/core/slot/deps.d.ts +0 -15
  276. package/dist/core/slot/deps.js +0 -88
  277. package/dist/core/slot/derived.d.ts +0 -32
  278. package/dist/core/slot/derived.js +0 -115
  279. package/dist/core/slot/platform-session-fields.d.ts +0 -38
  280. package/dist/core/slot/platform-session-fields.js +0 -48
  281. package/dist/core/slot/sf-schema.d.ts +0 -23
  282. package/dist/core/slot/sf-schema.js +0 -125
  283. package/dist/core/slot/sf-validate.d.ts +0 -14
  284. package/dist/core/slot/sf-validate.js +0 -132
  285. package/dist/core/slot/snapshot.d.ts +0 -18
  286. package/dist/core/slot/snapshot.js +0 -138
  287. package/dist/core/slot/write.d.ts +0 -48
  288. package/dist/core/slot/write.js +0 -426
  289. package/dist/core/stage/builders.d.ts +0 -54
  290. package/dist/core/stage/builders.js +0 -31
  291. package/dist/core/stage/condition.d.ts +0 -17
  292. package/dist/core/stage/condition.js +0 -198
  293. package/dist/core/stage/drive.d.ts +0 -43
  294. package/dist/core/stage/drive.js +0 -78
  295. package/dist/core/stage/finish-when.d.ts +0 -20
  296. package/dist/core/stage/finish-when.js +0 -82
  297. package/dist/core/stage/flatten.d.ts +0 -27
  298. package/dist/core/stage/flatten.js +0 -466
  299. package/dist/core/stage/index.d.ts +0 -15
  300. package/dist/core/stage/index.js +0 -13
  301. package/dist/core/stage/interrupts.d.ts +0 -28
  302. package/dist/core/stage/interrupts.js +0 -120
  303. package/dist/core/stage/ivr-fields.d.ts +0 -9
  304. package/dist/core/stage/ivr-fields.js +0 -75
  305. package/dist/core/stage/simulate.d.ts +0 -24
  306. package/dist/core/stage/simulate.js +0 -33
  307. package/dist/core/stage/transfer-fields.d.ts +0 -19
  308. package/dist/core/stage/transfer-fields.js +0 -129
  309. package/dist/core/stage/transfer-spec.d.ts +0 -48
  310. package/dist/core/stage/transfer-spec.js +0 -113
  311. package/dist/core/stage/types.d.ts +0 -208
  312. package/dist/core/stage/types.js +0 -2
  313. package/dist/core/stage/validate.d.ts +0 -11
  314. package/dist/core/stage/validate.js +0 -330
  315. package/dist/core/stage/walk.d.ts +0 -39
  316. package/dist/core/stage/walk.js +0 -119
  317. package/dist/fn-context/rpc/integration/list-bindings.d.ts +0 -21
  318. package/dist/fn-context/system-args.d.ts +0 -6
  319. package/dist/fn-context/system-args.js +0 -14
@@ -0,0 +1,780 @@
1
+ /**
2
+ * What a `db` read means, apart from where its rows are stored (15 §10.4, 18-record-search-index): the
3
+ * rows a page script and `ctx.api.db` see (`RowOut`, one `SlotOut` per field), what a `where`,
4
+ * `orderBy`, `after` / `skip` / `take` and `select` do to them, what a read may not say, and which of its
5
+ * conditions an index answers. The platform's reader (services `slot/page/records.ts`) loads its
6
+ * candidate rows from the database and applies these; the runtime's page unit tests
7
+ * (`testing.ts`) load them from the test's data and apply the same functions.
8
+ */
9
+ import { getCompiledTable, getTableField } from '../slot/compile-schema.js';
10
+ import { isRowTimeKey } from '../slot/schema.js';
11
+ export const RECORDS_LIMITS = {
12
+ /** Rows one call may reach through its anchor or the index, before any other filter. */
13
+ rowsPerCall: 500,
14
+ /** `findMany` without `take`. */
15
+ defaultTake: 100,
16
+ /** `skip`: further on, page with `after`. */
17
+ maxSkip: 1_000,
18
+ /** Values of one `in` / `notIn`. */
19
+ listValues: 500,
20
+ /** `count` counts up to this many; more answer this. */
21
+ countCap: 10_000,
22
+ /** Rows read by one reader (a page run; one `ctx.api.db` call). */
23
+ rowsPerRun: 5_000,
24
+ /**
25
+ * Stored row data (its JSON, UTF-8 bytes) one batch of calls may read (one `ctx.api.db` call, one
26
+ * sandbox slice), and one reader: what keeps a read from filling this process's memory, checked
27
+ * before the rows are loaded (34 §8).
28
+ */
29
+ bytesPerCall: 10 * 1024 * 1024,
30
+ bytesPerRun: 32 * 1024 * 1024,
31
+ /** One statement of a read, in milliseconds: past it the read fails as `query_timeout` (34 §8). */
32
+ statementMs: 3_000,
33
+ includeDepth: 3,
34
+ };
35
+ export class RecordsQueryError extends Error {
36
+ code;
37
+ constructor(message, code = 'invalid_read') {
38
+ super(message);
39
+ this.code = code;
40
+ this.name = 'RecordsQueryError';
41
+ }
42
+ }
43
+ export function limitExceeded(message) {
44
+ return new RecordsQueryError(message, 'limit_exceeded');
45
+ }
46
+ export const COMBINATORS = new Set(['AND', 'OR', 'NOT']);
47
+ /** Prisma's list filters, on a way back (`relatedName`). */
48
+ export const WAY_BACK_FILTERS = new Set(['some', 'every', 'none']);
49
+ /** What an `include` of a way back may say of its rows. */
50
+ export const RELATION_INCLUDE_KEYS = new Set(['where', 'orderBy', 'skip', 'take', 'include']);
51
+ /** A `multi_select`'s filters, as Prisma's scalar lists (27 §3.2). */
52
+ const LIST_OPERATORS = ['has', 'hasSome', 'hasEvery', 'isEmpty'];
53
+ const OPERATORS = new Set(['equals', 'in', 'notIn', 'not', 'lt', 'lte', 'gt', 'gte', 'startsWith', 'mode', 'state', ...LIST_OPERATORS]);
54
+ /** Searching inside a text: no index serves it on millions of rows, so `db` has none (34 §10). */
55
+ const SEARCH_OPERATORS = new Set(['contains', 'endsWith']);
56
+ /**
57
+ * Refuses `contains` / `endsWith` as an operator anywhere in a where (under AND / OR / NOT, a `not`, a
58
+ * relation's filter), saying what to write instead; before anything is read, whatever path the read
59
+ * takes. A field may be named `contains`: the where's own keys are fields, only a condition's are
60
+ * operators.
61
+ */
62
+ export function refuseSearch(tableName, where, path = 'where') {
63
+ if (!isPlainObject(where))
64
+ return;
65
+ for (const [key, value] of Object.entries(where)) {
66
+ const at = `${path}.${key}`;
67
+ if (COMBINATORS.has(key)) {
68
+ if (Array.isArray(value))
69
+ value.forEach((part, i) => refuseSearch(tableName, part, `${at}[${i}]`));
70
+ else
71
+ refuseSearch(tableName, value, at);
72
+ }
73
+ else {
74
+ refuseSearchOperators(tableName, value, at);
75
+ }
76
+ }
77
+ }
78
+ /** A field's condition: its operators, and the where inside a `not` or a relation's filter. */
79
+ function refuseSearchOperators(tableName, cond, path) {
80
+ if (!isPlainObject(cond))
81
+ return;
82
+ for (const [op, operand] of Object.entries(cond)) {
83
+ if (SEARCH_OPERATORS.has(op)) {
84
+ throw new RecordsQueryError(`records.${tableName}: ${path}.${op} is not supported: find by startsWith (mode: 'insensitive' for any case) or an exact field (a phone, an email, a code); or read the rows narrowed another way and look inside the text in code`);
85
+ }
86
+ if (op === 'not')
87
+ refuseSearchOperators(tableName, operand, `${path}.not`);
88
+ else if (op === 'is' || op === 'isNot' || WAY_BACK_FILTERS.has(op))
89
+ refuseSearch(tableName, operand, `${path}.${op}`);
90
+ }
91
+ }
92
+ /** The fields of a read's conditions and order that are index entries (not row times): their dictionary ids are needed. */
93
+ export function indexedFieldsOf(conditions, order) {
94
+ return [...conditions.filter((cond) => !cond.row).map((cond) => cond.field), ...(order && !order.row ? [order.field] : [])];
95
+ }
96
+ /**
97
+ * The conditions of a where that pin it to some rows (top level and top-level AND): `id` or a ref
98
+ * field equal to / in some ids (15 §10.6.1).
99
+ */
100
+ export function findAnchors(table, where) {
101
+ const out = [];
102
+ const visit = (w) => {
103
+ for (const [field, cond] of Object.entries(w)) {
104
+ if (field === 'AND') {
105
+ for (const part of asArray(cond))
106
+ if (isPlainObject(part))
107
+ visit(part);
108
+ continue;
109
+ }
110
+ const def = getTableField(table, field);
111
+ if (field !== 'id' && !def?.ref)
112
+ continue;
113
+ const ids = anchorIds(cond);
114
+ if (ids)
115
+ out.push({ field, ids });
116
+ }
117
+ };
118
+ visit(where);
119
+ // `id` first: the primary key is the narrowest; then refs.
120
+ return out.sort((a, b) => (a.field === 'id' ? 0 : 1) - (b.field === 'id' ? 0 : 1));
121
+ }
122
+ function anchorIds(cond) {
123
+ if (typeof cond === 'string')
124
+ return [cond];
125
+ if (!isPlainObject(cond))
126
+ return null;
127
+ const c = cond;
128
+ if (typeof c.equals === 'string')
129
+ return [c.equals];
130
+ if (Array.isArray(c.in) && c.in.every((v) => typeof v === 'string'))
131
+ return c.in;
132
+ return null;
133
+ }
134
+ export function relationFilters(table, where) {
135
+ const out = [];
136
+ const visit = (w) => {
137
+ if (!isPlainObject(w))
138
+ return;
139
+ for (const [field, cond] of Object.entries(w)) {
140
+ if (COMBINATORS.has(field)) {
141
+ for (const part of asArray(cond))
142
+ visit(part);
143
+ continue;
144
+ }
145
+ const ref = getTableField(table, field)?.ref;
146
+ if (ref && isPlainObject(cond) && ('is' in cond || 'isNot' in cond)) {
147
+ const c = cond;
148
+ out.push({ field, target: ref.table, sub: [c.is, c.isNot].filter((w) => w !== undefined && w !== null) });
149
+ }
150
+ }
151
+ };
152
+ visit(where);
153
+ return out;
154
+ }
155
+ /** `some` / `every` / `none` on a way back (Prisma's list filters): over the rows pointing at `row`. */
156
+ function matchWayBack(context, tableName, row, relation, cond) {
157
+ const keys = isPlainObject(cond) ? Object.keys(cond) : [];
158
+ if (keys.length === 0 || keys.some((key) => !WAY_BACK_FILTERS.has(key))) {
159
+ throw new RecordsQueryError(`records.${tableName}: where.${relation.name} takes some / every / none`);
160
+ }
161
+ const c = cond;
162
+ const rows = context.related.get(relatedKey(tableName, relation.name))?.get(row.id) ?? [];
163
+ const hit = (where) => (child) => matches(context, relation.table, child, where);
164
+ if ('some' in c && !rows.some(hit(c.some)))
165
+ return false;
166
+ if ('every' in c && !rows.every(hit(c.every)))
167
+ return false;
168
+ if ('none' in c && rows.some(hit(c.none)))
169
+ return false;
170
+ return true;
171
+ }
172
+ /** The ways back a `where` filters by (top level and inside AND / OR / NOT), with their sub-wheres. */
173
+ export function wayBackFilters(tableName, where, relationOf) {
174
+ const out = [];
175
+ const visit = (w) => {
176
+ if (!isPlainObject(w))
177
+ return;
178
+ for (const [key, cond] of Object.entries(w)) {
179
+ if (COMBINATORS.has(key)) {
180
+ for (const part of asArray(cond))
181
+ visit(part);
182
+ continue;
183
+ }
184
+ const relation = key === 'id' || isRowTimeKey(key) ? undefined : relationOf(tableName, key);
185
+ if (!relation || !isPlainObject(cond))
186
+ continue;
187
+ out.push({ relation, sub: [...WAY_BACK_FILTERS].map((op) => cond[op]).filter((sub) => sub !== undefined && sub !== null) });
188
+ }
189
+ };
190
+ visit(where);
191
+ return out;
192
+ }
193
+ export function relatedKey(tableName, name) {
194
+ return `${tableName}\u0000${name}`;
195
+ }
196
+ export function matches(context, tableName, row, where) {
197
+ if (where === undefined || where === null)
198
+ return true;
199
+ if (!isPlainObject(where))
200
+ throw new RecordsQueryError(`records.${tableName}: where must be an object`);
201
+ const table = getCompiledTable(context.schema, tableName);
202
+ for (const [field, cond] of Object.entries(where)) {
203
+ if (cond === undefined)
204
+ continue;
205
+ if (field === 'AND') {
206
+ if (!asArray(cond).every((part) => matches(context, tableName, row, part)))
207
+ return false;
208
+ continue;
209
+ }
210
+ if (field === 'OR') {
211
+ if (!asArray(cond).some((part) => matches(context, tableName, row, part)))
212
+ return false;
213
+ continue;
214
+ }
215
+ if (field === 'NOT') {
216
+ if (asArray(cond).some((part) => matches(context, tableName, row, part)))
217
+ return false;
218
+ continue;
219
+ }
220
+ if (field === 'id') {
221
+ if (!matchValue(row.id, cond, tableName, field))
222
+ return false;
223
+ continue;
224
+ }
225
+ if (isRowTimeKey(field)) {
226
+ const { same, order } = comparisonOf('datetime');
227
+ if (!matchValue(row[field] ?? null, cond, tableName, field, same, order))
228
+ return false;
229
+ continue;
230
+ }
231
+ const def = getTableField(table, field);
232
+ if (!def) {
233
+ const relation = context.relationOf(tableName, field);
234
+ if (!relation)
235
+ throw new RecordsQueryError(`records.${tableName}: where.${field} is not a field of this table`);
236
+ if (!matchWayBack(context, tableName, row, relation, cond))
237
+ return false;
238
+ continue;
239
+ }
240
+ const slot = row[field];
241
+ if (def.ref && isPlainObject(cond) && ('is' in cond || 'isNot' in cond)) {
242
+ const c = cond;
243
+ const target = typeof slot.value === 'string' ? context.targets.get(slot.value) : undefined;
244
+ if ('is' in c) {
245
+ if (c.is === null ? target !== undefined : !target || !matches(context, def.ref.table, target, c.is))
246
+ return false;
247
+ }
248
+ if ('isNot' in c) {
249
+ if (c.isNot === null ? target === undefined : target && matches(context, def.ref.table, target, c.isNot))
250
+ return false;
251
+ }
252
+ continue;
253
+ }
254
+ // A date or time compares as the instant it names, as the field index does (18-record-search-index
255
+ // §4.6); a phone / email operand is already in stored form (normalizeWhereValues).
256
+ const { same, order } = comparisonOf(def.dataType);
257
+ if (isPlainObject(cond) && 'state' in cond) {
258
+ const { state, ...rest } = cond;
259
+ if (!matchValue(slot.state, state, tableName, `${field}.state`))
260
+ return false;
261
+ if (Object.keys(rest).length > 0 && !matchValue(slot.value, rest, tableName, field, same, order))
262
+ return false;
263
+ continue;
264
+ }
265
+ if (!matchValue(slot.value, cond, tableName, field, same, order))
266
+ return false;
267
+ }
268
+ return true;
269
+ }
270
+ /**
271
+ * Prisma scalar filter: a plain value is `equals`; `null` matches an empty field. `same` is how
272
+ * equality (`equals` / `in` / `notIn`) compares: as JSON, or an identifier normalized.
273
+ */
274
+ function matchValue(value, cond, tableName, field, same = equal, order = compare) {
275
+ if (!isPlainObject(cond))
276
+ return same(value, cond);
277
+ const { mode } = cond;
278
+ if (mode !== undefined && mode !== 'insensitive' && mode !== 'default') {
279
+ throw new RecordsQueryError(`records.${tableName}: where.${field}.mode is 'insensitive' or 'default'`);
280
+ }
281
+ // `mode: 'insensitive'`: strings compare lower-cased, as Prisma does.
282
+ if (mode === 'insensitive') {
283
+ const fold = (v) => (typeof v === 'string' ? v.toLowerCase() : Array.isArray(v) ? v.map(fold) : v);
284
+ const { mode: _, ...ops } = cond;
285
+ const folded = Object.fromEntries(Object.entries(ops).map(([op, operand]) => [op, op === 'not' ? operand : fold(operand)]));
286
+ return matchValue(fold(value), folded, tableName, field, same, order);
287
+ }
288
+ for (const [op, operand] of Object.entries(cond)) {
289
+ if (!OPERATORS.has(op))
290
+ throw new RecordsQueryError(`records.${tableName}: where.${field}.${op} is not supported`);
291
+ switch (op) {
292
+ case 'mode':
293
+ break;
294
+ case 'equals':
295
+ if (!same(value, operand))
296
+ return false;
297
+ break;
298
+ case 'in':
299
+ if (!asArray(operand).some((v) => same(value, v)))
300
+ return false;
301
+ break;
302
+ case 'notIn':
303
+ if (asArray(operand).some((v) => same(value, v)))
304
+ return false;
305
+ break;
306
+ case 'not':
307
+ if (matchValue(value, operand, tableName, field, same, order))
308
+ return false;
309
+ break;
310
+ case 'lt':
311
+ case 'lte':
312
+ case 'gt':
313
+ case 'gte': {
314
+ if (value === null || value === undefined || operand === null || operand === undefined)
315
+ return false;
316
+ const c = order(value, operand);
317
+ if (op === 'lt' && !(c < 0))
318
+ return false;
319
+ if (op === 'lte' && !(c <= 0))
320
+ return false;
321
+ if (op === 'gt' && !(c > 0))
322
+ return false;
323
+ if (op === 'gte' && !(c >= 0))
324
+ return false;
325
+ break;
326
+ }
327
+ case 'startsWith':
328
+ if (typeof value !== 'string' || !value.startsWith(String(operand)))
329
+ return false;
330
+ break;
331
+ case 'has':
332
+ case 'hasSome':
333
+ case 'hasEvery':
334
+ case 'isEmpty': {
335
+ // A multi_select's list; an empty field is no list (it has nothing and is not "none of them").
336
+ if (!Array.isArray(value))
337
+ return false;
338
+ const items = asArray(operand);
339
+ if (op === 'has' && !value.some((v) => same(v, operand)))
340
+ return false;
341
+ if (op === 'hasSome' && !items.some((o) => value.some((v) => same(v, o))))
342
+ return false;
343
+ if (op === 'hasEvery' && !items.every((o) => value.some((v) => same(v, o))))
344
+ return false;
345
+ if (op === 'isEmpty' && (value.length === 0) !== (operand === true))
346
+ return false;
347
+ break;
348
+ }
349
+ case 'state':
350
+ throw new RecordsQueryError(`records.${tableName}: where.${field}.state goes on the field, not inside an operator`);
351
+ }
352
+ }
353
+ return true;
354
+ }
355
+ function equal(a, b) {
356
+ if (b === null)
357
+ return a === null || a === undefined;
358
+ return JSON.stringify(a) === JSON.stringify(b);
359
+ }
360
+ /**
361
+ * How values of a field type compare: a `date` / `datetime` as the instant it names (`…00.000Z` and
362
+ * `…00Z` are one time; an offset counts; `YYYY` / `YYYY-MM` at their start), as the field index
363
+ * compares them; a value that is not one compares as text. Everything else as JSON / text.
364
+ */
365
+ function comparisonOf(dataType) {
366
+ if (dataType !== 'date' && dataType !== 'datetime')
367
+ return { same: equal, order: compare };
368
+ const instant = (v) => (typeof v === 'string' ? indexValueOf(dataType, v)?.valueTime?.getTime() : undefined);
369
+ return {
370
+ same: (a, b) => {
371
+ if (b === null || a === null || a === undefined)
372
+ return equal(a, b);
373
+ const ia = instant(a);
374
+ const ib = instant(b);
375
+ return ia !== undefined && ib !== undefined ? ia === ib : equal(a, b);
376
+ },
377
+ order: (a, b) => {
378
+ const ia = instant(a);
379
+ const ib = instant(b);
380
+ return ia !== undefined && ib !== undefined ? ia - ib : compare(a, b);
381
+ },
382
+ };
383
+ }
384
+ function compare(a, b) {
385
+ if (typeof a === 'number' && typeof b === 'number')
386
+ return a - b;
387
+ return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
388
+ }
389
+ export function orderKeys(table, orderBy) {
390
+ if (orderBy === undefined || orderBy === null)
391
+ return [];
392
+ return asArray(orderBy).flatMap((entry) => {
393
+ if (!isPlainObject(entry))
394
+ throw new RecordsQueryError('records: orderBy is { field: "asc" | "desc" } or an array of those');
395
+ return Object.entries(entry).map(([field, dir]) => {
396
+ const system = field === 'id' || isRowTimeKey(field);
397
+ const def = system ? undefined : getTableField(table, field);
398
+ if (!system && !def)
399
+ throw new RecordsQueryError(`records: orderBy.${field} is not a field`);
400
+ if (dir !== 'asc' && dir !== 'desc')
401
+ throw new RecordsQueryError(`records: orderBy.${field} is "asc" or "desc"`);
402
+ const order = def ? comparisonOf(def.dataType).order : isRowTimeKey(field) ? comparisonOf('datetime').order : compare;
403
+ return { field, sign: dir === 'asc' ? 1 : -1, order, ...(def ? { def } : {}) };
404
+ });
405
+ });
406
+ }
407
+ function placeOf(keys, row) {
408
+ const values = keys.map(({ field }) => (field === 'id' || isRowTimeKey(field) ? row[field] : row[field]?.value));
409
+ return { values, created: Date.parse(row.createdAt ?? '') || 0, id: row.id };
410
+ }
411
+ /** The read's total order: the keys, empty values last either way; then creation, then id. */
412
+ function comparePlaces(keys, a, b) {
413
+ for (const [i, { sign, order }] of keys.entries()) {
414
+ const va = a.values[i];
415
+ const vb = b.values[i];
416
+ const ea = va === null || va === undefined;
417
+ const eb = vb === null || vb === undefined;
418
+ if (ea || eb) {
419
+ if (ea !== eb)
420
+ return ea ? 1 : -1;
421
+ continue;
422
+ }
423
+ const c = order(va, vb);
424
+ if (c !== 0)
425
+ return c * sign;
426
+ }
427
+ return a.created - b.created || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
428
+ }
429
+ /** Rows in the read's order; without keys, as they come (creation order). */
430
+ export function orderRows(keys, rows) {
431
+ if (keys.length === 0)
432
+ return rows;
433
+ return rows
434
+ .map((row) => ({ row, place: placeOf(keys, row) }))
435
+ .sort((a, b) => comparePlaces(keys, a.place, b.place))
436
+ .map(({ row }) => row);
437
+ }
438
+ /**
439
+ * `orderBy` as the database can order: null without one, the field when it is one indexed field or a
440
+ * row time; undefined when it cannot (several keys, `id`, a field not indexed).
441
+ */
442
+ export function indexOrder(table, orderBy) {
443
+ if (orderBy === undefined || orderBy === null)
444
+ return null;
445
+ const entries = (Array.isArray(orderBy) ? orderBy : [orderBy]).flatMap((entry) => isPlainObject(entry) ? Object.entries(entry) : [['', '']]);
446
+ if (entries.length !== 1)
447
+ return undefined;
448
+ const [field, dir] = entries[0];
449
+ if (isRowTimeKey(field))
450
+ return dir === 'asc' || dir === 'desc' ? { field, dataType: 'datetime', desc: dir === 'desc', row: true } : undefined;
451
+ const def = field === 'id' ? undefined : getTableField(table, field);
452
+ if (!def || def.ref || def.dataType === 'text' || def.dataType === 'multi_select' || def.dataType === 'contact_schedule' || (dir !== 'asc' && dir !== 'desc'))
453
+ return undefined;
454
+ return { field, dataType: def.dataType, desc: dir === 'desc' };
455
+ }
456
+ /** Rows in the read's order, paged: from strictly after `place`, then `skip`, then `take`. */
457
+ export function pageRows(keys, rows, place, args) {
458
+ let start = 0;
459
+ if (place) {
460
+ start = rows.findIndex((row) => comparePlaces(keys, placeOf(keys, row), place) > 0);
461
+ if (start < 0)
462
+ return [];
463
+ }
464
+ const skip = skipOf(args.skip);
465
+ const take = optionalCount(args.take, 'take') ?? RECORDS_LIMITS.defaultTake;
466
+ if (take > RECORDS_LIMITS.rowsPerCall)
467
+ throw limitExceeded(`records: take is at most ${RECORDS_LIMITS.rowsPerCall}`);
468
+ return rows.slice(start + skip, start + skip + take);
469
+ }
470
+ /**
471
+ * `after`: the place a page starts after, read off a row as the reader returned it (normally the last
472
+ * of the page before). Taken by value, never looked up: its id only breaks ties, so a row since deleted,
473
+ * changed or of another tenant is only a place and discloses nothing. It needs the row's `id` and
474
+ * `createdAt`, and its value of each order key: `updatedAt`, or the field's slot (`{ value }`, a null
475
+ * value for none). A field's value must be of its type ({@link fitsField}): one the index cannot hold
476
+ * would otherwise place as "no value" in SQL and compare as text in memory.
477
+ */
478
+ export function afterOf(tableName, keys, args) {
479
+ if (args.cursor !== undefined) {
480
+ throw new RecordsQueryError(`records.${tableName}: cursor is now after: pass the last row read (after: rows.at(-1)) to read the rows after it`);
481
+ }
482
+ const after = args.after;
483
+ if (after === undefined)
484
+ return undefined;
485
+ if (!isPlainObject(after))
486
+ throw new RecordsQueryError(`records.${tableName}: after is a row as read (the last row of the page before)`);
487
+ if (!isUuid(after.id))
488
+ throw new RecordsQueryError(`records.${tableName}: after needs the row's id`);
489
+ const created = typeof after.createdAt === 'string' ? Date.parse(after.createdAt) : NaN;
490
+ if (Number.isNaN(created))
491
+ throw new RecordsQueryError(`records.${tableName}: after needs createdAt (ISO 8601), as the row was read`);
492
+ const id = after.id.toLowerCase();
493
+ const values = keys.map(({ field, def }) => {
494
+ if (field === 'id')
495
+ return id;
496
+ if (isRowTimeKey(field)) {
497
+ const time = after[field];
498
+ if (typeof time !== 'string' || Number.isNaN(Date.parse(time))) {
499
+ throw new RecordsQueryError(`records.${tableName}: after needs ${field} (ISO 8601), as the row was read: the read is ordered by it`);
500
+ }
501
+ return time;
502
+ }
503
+ const slot = after[field];
504
+ if (!isPlainObject(slot))
505
+ throw new RecordsQueryError(`records.${tableName}: after needs ${field}: the read is ordered by it; keep it in select`);
506
+ const value = slot.value ?? null;
507
+ if (value !== null && def && !fitsField(def, value)) {
508
+ throw new RecordsQueryError(`records.${tableName}: after.${field} holds ${shownValue(value)}, not ${typeNameOf(def)}: ${field} is a ${def.ref ? 'ref' : def.dataType} field; pass the row as read (a null value for none)`);
509
+ }
510
+ return value;
511
+ });
512
+ return { values, created, id };
513
+ }
514
+ /**
515
+ * Whether `value` is one of `def`'s type, as `after`'s value of an order key: on a field the index
516
+ * holds, exactly what its entry can hold (`indexValueOf`, as the database orders by it), so the SQL and
517
+ * in-memory reads take the same places; on any other (read in memory only), the shape its rows carry.
518
+ */
519
+ function fitsField(def, value) {
520
+ if (def.ref)
521
+ return typeof value === 'string';
522
+ switch (def.dataType) {
523
+ case 'text':
524
+ return typeof value === 'string';
525
+ case 'multi_select':
526
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
527
+ case 'contact_schedule':
528
+ return isPlainObject(value);
529
+ default:
530
+ return indexValueOf(def.dataType, value) !== null;
531
+ }
532
+ }
533
+ /** What {@link fitsField} takes, in words. */
534
+ function typeNameOf(def) {
535
+ if (def.ref)
536
+ return 'an id';
537
+ switch (def.dataType) {
538
+ case 'number':
539
+ return 'a number';
540
+ case 'boolean':
541
+ return 'a boolean';
542
+ case 'date':
543
+ return 'a date (YYYY-MM-DD, YYYY-MM or YYYY)';
544
+ case 'datetime':
545
+ return 'a date and time (ISO 8601)';
546
+ case 'multi_select':
547
+ return 'a list of options';
548
+ case 'contact_schedule':
549
+ return 'a contact schedule';
550
+ default:
551
+ return 'a string';
552
+ }
553
+ }
554
+ /** A value in a message: a scalar as written (cut short), a list or an object by its kind. */
555
+ function shownValue(value) {
556
+ if (Array.isArray(value))
557
+ return 'a list';
558
+ if (typeof value === 'object')
559
+ return 'an object';
560
+ const text = typeof value === 'string' ? JSON.stringify(value) : String(value);
561
+ return text.length > 40 ? `${text.slice(0, 37)}...` : text;
562
+ }
563
+ export function skipOf(value) {
564
+ const skip = optionalCount(value, 'skip') ?? 0;
565
+ if (skip > RECORDS_LIMITS.maxSkip)
566
+ throw limitExceeded(`records: skip is at most ${RECORDS_LIMITS.maxSkip}; page further with after`);
567
+ return skip;
568
+ }
569
+ /** Every `in` / `notIn` of a where (and its AND / OR / NOT, way-back and `is` filters) within `listValues`. */
570
+ export function checkListSizes(tableName, where) {
571
+ if (Array.isArray(where)) {
572
+ for (const part of where)
573
+ checkListSizes(tableName, part);
574
+ return;
575
+ }
576
+ if (!isPlainObject(where))
577
+ return;
578
+ for (const [key, value] of Object.entries(where)) {
579
+ if ((key === 'in' || key === 'notIn') && Array.isArray(value) && value.length > RECORDS_LIMITS.listValues) {
580
+ throw limitExceeded(`records.${tableName}: ${key} takes at most ${RECORDS_LIMITS.listValues} values`);
581
+ }
582
+ checkListSizes(tableName, value);
583
+ }
584
+ }
585
+ export function optionalCount(value, name) {
586
+ if (value === undefined)
587
+ return undefined;
588
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
589
+ throw new RecordsQueryError(`records: ${name} is a whole number ≥ 0`);
590
+ }
591
+ return value;
592
+ }
593
+ export function selectFields(tableName, table, row, select) {
594
+ if (select === undefined || select === null)
595
+ return row;
596
+ if (!isPlainObject(select))
597
+ throw new RecordsQueryError(`records.${tableName}: select must be an object`);
598
+ // The id and the row's times always come.
599
+ const out = { id: row.id, ...(row.createdAt ? { createdAt: row.createdAt, updatedAt: row.updatedAt } : {}) };
600
+ for (const [field, on] of Object.entries(select)) {
601
+ if (!on || field === 'id' || isRowTimeKey(field))
602
+ continue;
603
+ if (!getTableField(table, field))
604
+ throw new RecordsQueryError(`records.${tableName}: select.${field} is not a field`);
605
+ out[field] = row[field];
606
+ }
607
+ // The ways back `include`d stay: `select` picks among the fields.
608
+ for (const [key, value] of Object.entries(row))
609
+ if (Array.isArray(value))
610
+ out[key] = value;
611
+ return out;
612
+ }
613
+ export function toSlot(table, id, field, value) {
614
+ const stored = value.state !== 'empty';
615
+ return {
616
+ value: value.value ?? null,
617
+ state: value.state,
618
+ reason: value.reason ?? null,
619
+ updatedAt: stored && value.updatedAt ? new Date(value.updatedAt).toISOString() : null,
620
+ ref: { table, id, field },
621
+ };
622
+ }
623
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
624
+ export function isUuid(value) {
625
+ return typeof value === 'string' && UUID.test(value);
626
+ }
627
+ export function asArray(value) {
628
+ return Array.isArray(value) ? value : [value];
629
+ }
630
+ export function isPlainObject(value) {
631
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
632
+ }
633
+ // ---------------------------------------------------------------------------------------------
634
+ // The field index: which fields it holds, a value's index columns, and the conditions it answers.
635
+ const PROJECTED_TYPES = new Set(['string', 'select', 'time', 'phone', 'email', 'number', 'boolean', 'date', 'datetime']);
636
+ /** The fields of one table's definition that go to the index. */
637
+ export function tableIndexedFields(fields) {
638
+ const out = new Map();
639
+ for (const [fieldName, field] of Object.entries(fields)) {
640
+ if (fieldName === 'id')
641
+ continue;
642
+ const def = field;
643
+ if (def.ref || !def.dataType || !PROJECTED_TYPES.has(def.dataType))
644
+ continue;
645
+ out.set(fieldName, { dataType: def.dataType, unique: def.unique === true });
646
+ }
647
+ return out;
648
+ }
649
+ /** A stored value's index columns; null when it has none to index (empty, or not of its type). */
650
+ export function indexValueOf(dataType, value) {
651
+ if (value === null || value === undefined)
652
+ return null;
653
+ const none = { valueText: null, valueNum: null, valueTime: null };
654
+ switch (dataType) {
655
+ case 'string':
656
+ case 'select':
657
+ // `HH:mm`, zero-padded: ordered as text is ordered in time (27 §4).
658
+ case 'time':
659
+ case 'phone':
660
+ case 'email':
661
+ return typeof value === 'string' ? { ...none, valueText: value } : null;
662
+ case 'boolean':
663
+ return typeof value === 'boolean' ? { ...none, valueText: String(value) } : null;
664
+ case 'number':
665
+ return typeof value === 'number' && Number.isFinite(value) ? { ...none, valueNum: value } : null;
666
+ case 'date': {
667
+ // `YYYY` / `YYYY-MM` index at the start of their span.
668
+ if (typeof value !== 'string')
669
+ return null;
670
+ const [year, month = '01', day = '01'] = value.split('-');
671
+ const time = new Date(`${year}-${month}-${day}T00:00:00.000Z`);
672
+ return Number.isNaN(time.getTime()) ? null : { ...none, valueTime: time };
673
+ }
674
+ case 'datetime': {
675
+ if (typeof value !== 'string')
676
+ return null;
677
+ const time = new Date(value);
678
+ return Number.isNaN(time.getTime()) ? null : { ...none, valueTime: time };
679
+ }
680
+ default:
681
+ return null;
682
+ }
683
+ }
684
+ const INDEX_OPERATORS = new Set(['equals', 'in', 'lt', 'lte', 'gt', 'gte', 'startsWith', 'mode']);
685
+ /** What a condition on a row time can say in SQL: it is an instant. */
686
+ const TIME_OPERATORS = new Set(['equals', 'in', 'lt', 'lte', 'gt', 'gte']);
687
+ /** Why a condition cannot start a query on its own (it can still narrow one another condition starts). */
688
+ function cannotStart(cond) {
689
+ if ('startsWith' in cond.ops && String(cond.ops.startsWith).length === 0)
690
+ return `where.${cond.field}.startsWith is empty`;
691
+ return null;
692
+ }
693
+ /**
694
+ * The conditions of a where the database can answer (top level and top-level AND): on indexed fields
695
+ * and row times. And whether they are the whole where (nothing else to check in memory). A condition
696
+ * on a field that is not indexed, an empty value, `not`, `notIn`, `state`, `OR` and `NOT` are left to
697
+ * the in-memory filter.
698
+ */
699
+ export function indexConditions(table, where) {
700
+ const fields = tableIndexedFields(table.fields);
701
+ const conditions = [];
702
+ let whole = true;
703
+ const visit = (w) => {
704
+ for (const [field, cond] of Object.entries(w)) {
705
+ if (cond === undefined)
706
+ continue;
707
+ if (field === 'AND') {
708
+ for (const part of Array.isArray(cond) ? cond : [cond]) {
709
+ if (isPlainObject(part))
710
+ visit(part);
711
+ else
712
+ whole = false;
713
+ }
714
+ continue;
715
+ }
716
+ if (isRowTimeKey(field)) {
717
+ const ops = indexOps(cond);
718
+ if (!ops || !Object.keys(ops).every((op) => TIME_OPERATORS.has(op))) {
719
+ whole = false;
720
+ continue;
721
+ }
722
+ conditions.push({ field, dataType: 'datetime', ops, insensitive: false, row: true });
723
+ continue;
724
+ }
725
+ const indexed = fields.get(field);
726
+ const ops = indexed ? indexOps(cond) : null;
727
+ if (!indexed || !ops) {
728
+ whole = false;
729
+ continue;
730
+ }
731
+ const { mode, ...rest } = ops;
732
+ conditions.push({ field, dataType: indexed.dataType, ops: rest, insensitive: mode === 'insensitive' });
733
+ }
734
+ };
735
+ visit(where);
736
+ return { conditions, whole };
737
+ }
738
+ /** A condition's operators when the index can answer all of them; null otherwise. */
739
+ function indexOps(cond) {
740
+ if (cond === null)
741
+ return null;
742
+ if (!isPlainObject(cond))
743
+ return { equals: cond };
744
+ const ops = cond;
745
+ const keys = Object.keys(ops);
746
+ if (keys.length === 0 || !keys.every((op) => INDEX_OPERATORS.has(op)))
747
+ return null;
748
+ if (ops.mode !== undefined && ops.mode !== 'insensitive' && ops.mode !== 'default')
749
+ return null;
750
+ if (keys.every((op) => op === 'mode'))
751
+ return null;
752
+ for (const [op, operand] of Object.entries(ops)) {
753
+ if (op === 'mode')
754
+ continue;
755
+ if (operand === null || operand === undefined)
756
+ return null;
757
+ if (op === 'in' && (!Array.isArray(operand) || operand.some((v) => v === null)))
758
+ return null;
759
+ }
760
+ return ops;
761
+ }
762
+ /** The conditions that can start a query; when none can, why. */
763
+ export function startableConditions(conditions) {
764
+ const candidates = conditions.filter((cond) => !cannotStart(cond));
765
+ return { candidates, why: candidates.length > 0 ? null : (conditions.map(cannotStart).find((why) => why !== null) ?? null) };
766
+ }
767
+ /**
768
+ * Why a `where` with no anchor (`id` or a ref field equal to / in some ids) cannot be read: no condition
769
+ * on an indexed field or a row time starts it. Null when one does. The platform's reader and the page
770
+ * unit tests refuse the same reads, with this message.
771
+ */
772
+ export function unstartableWhy(tableName, table, where) {
773
+ const { conditions } = indexConditions(table, where);
774
+ const { candidates, why } = startableConditions(conditions);
775
+ if (candidates.length > 0)
776
+ return null;
777
+ const indexed = Object.entries(table.fields).filter(([name, def]) => name !== 'id' && !def.ref && def.dataType !== 'text' && def.dataType !== 'multi_select' && def.dataType !== 'contact_schedule');
778
+ return `records.${tableName}: ${why ? `${why}; ` : ''}the where needs id or a ref field equal to / in some ids, or a condition on a field (${indexed.map(([name]) => name).join(', ') || 'none'}): equals, in, lt / lte / gt / gte or startsWith; or on createdAt / updatedAt: equals, in, lt / lte / gt / gte`;
779
+ }
780
+ //# sourceMappingURL=records-query.js.map