@primate/core 0.4.6 → 0.6.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 (556) hide show
  1. package/lib/private/App.d.ts +771 -6
  2. package/lib/private/App.js +49 -16
  3. package/lib/private/Binder.d.ts +2 -2
  4. package/lib/private/Flags.d.ts +8 -4
  5. package/lib/private/Module.d.ts +4 -16
  6. package/lib/private/Module.js +1 -17
  7. package/lib/private/app/EnvSchema.d.ts +5 -0
  8. package/lib/private/app/EnvSchema.js +2 -0
  9. package/lib/private/app/Facade.browser.d.ts +11 -0
  10. package/lib/private/app/Facade.browser.js +19 -0
  11. package/lib/private/app/Facade.d.ts +811 -0
  12. package/lib/private/app/Facade.js +56 -0
  13. package/lib/private/asset/Asset.d.ts +1 -1
  14. package/lib/private/build/App.d.ts +2 -2
  15. package/lib/private/build/App.js +0 -2
  16. package/lib/private/build/client/index.js +19 -10
  17. package/lib/private/build/client/plugin/app-request.d.ts +4 -0
  18. package/lib/private/build/client/plugin/app-request.js +19 -0
  19. package/lib/private/build/client/plugin/view.d.ts +4 -0
  20. package/lib/private/build/client/plugin/view.js +13 -0
  21. package/lib/private/build/hook.d.ts +1 -2
  22. package/lib/private/build/hook.js +25 -19
  23. package/lib/private/build/index.d.ts +2 -1
  24. package/lib/private/build/index.js +27 -27
  25. package/lib/private/build/presets.d.ts +10 -0
  26. package/lib/private/build/presets.js +39 -0
  27. package/lib/private/build/server/index.js +10 -14
  28. package/lib/private/build/server/plugin/app-request.d.ts +4 -0
  29. package/lib/private/build/server/plugin/app-request.js +19 -0
  30. package/lib/private/build/server/plugin/assets.js +9 -7
  31. package/lib/private/build/server/plugin/config.js +11 -4
  32. package/lib/private/build/server/plugin/frontend.js +4 -2
  33. package/lib/private/build/server/plugin/live-reload.d.ts +4 -0
  34. package/lib/private/build/server/plugin/{hot-reload.js → live-reload.js} +12 -12
  35. package/lib/private/build/server/plugin/native-addons.js +10 -11
  36. package/lib/private/build/server/plugin/node-imports.js +2 -2
  37. package/lib/private/build/server/plugin/route.js +4 -4
  38. package/lib/private/build/server/plugin/store.js +11 -33
  39. package/lib/private/build/server/plugin/stores.js +12 -9
  40. package/lib/private/build/server/plugin/view.js +5 -5
  41. package/lib/private/build/server/plugin/views.js +4 -4
  42. package/lib/private/build/server/plugin/virtual-pages.js +8 -8
  43. package/lib/private/build/server/plugin/virtual-routes.js +8 -13
  44. package/lib/private/build/server/plugin/wasm.js +2 -3
  45. package/lib/private/build/shared/plugin/app-request.d.ts +4 -0
  46. package/lib/private/build/shared/plugin/app-request.js +19 -0
  47. package/lib/private/bye.js +2 -3
  48. package/lib/private/client/Data.d.ts +3 -2
  49. package/lib/private/client/Render.d.ts +8 -0
  50. package/lib/private/{frontend → client}/ServerData.d.ts +1 -1
  51. package/lib/private/client/ServerView.d.ts +4 -0
  52. package/lib/private/client/ValidateInit.d.ts +1 -3
  53. package/lib/private/client/ValidationError.d.ts +1 -1
  54. package/lib/private/{frontend → client}/View.d.ts +1 -1
  55. package/lib/private/{frontend → client}/ViewOptions.d.ts +1 -1
  56. package/lib/private/{frontend → client}/ViewResponse.d.ts +2 -2
  57. package/lib/private/client/app.js +3 -3
  58. package/lib/private/client/boot.d.ts +5 -0
  59. package/lib/private/client/boot.js +64 -0
  60. package/lib/private/client/create-form.d.ts +32 -0
  61. package/lib/private/client/create-form.js +123 -0
  62. package/lib/private/client/extract-issues.d.ts +4 -0
  63. package/lib/private/client/extract-issues.js +21 -0
  64. package/lib/private/client/http.d.ts +13 -0
  65. package/lib/private/client/http.js +57 -0
  66. package/lib/private/client/index.d.ts +23 -0
  67. package/lib/private/client/index.js +16 -0
  68. package/lib/private/client/navigate.d.ts +13 -0
  69. package/lib/private/client/navigate.js +67 -0
  70. package/lib/private/client/root.d.ts +9 -0
  71. package/lib/private/client/root.js +11 -0
  72. package/lib/private/client/submit.d.ts +2 -0
  73. package/lib/private/client/submit.js +41 -0
  74. package/lib/private/client/{toValidated.d.ts → to-validated.d.ts} +2 -4
  75. package/lib/private/client/{toValidated.js → to-validated.js} +1 -1
  76. package/lib/private/client/validate-field.d.ts +3 -0
  77. package/lib/private/client/validate-field.js +41 -0
  78. package/lib/private/config/index.d.ts +7 -21
  79. package/lib/private/config/index.js +4 -1
  80. package/lib/private/config/schema.d.ts +82 -22
  81. package/lib/private/config/schema.js +25 -7
  82. package/lib/private/cookie.d.ts +12 -6
  83. package/lib/private/cookie.js +10 -14
  84. package/lib/private/db/As.d.ts +10 -0
  85. package/lib/private/db/DB.d.ts +49 -0
  86. package/lib/private/db/DB.js +3 -0
  87. package/lib/private/db/DataDict.d.ts +5 -0
  88. package/lib/private/db/DataKey.d.ts +4 -0
  89. package/lib/private/db/DataValue.d.ts +17 -0
  90. package/lib/private/db/MemoryDB.d.ts +33 -0
  91. package/lib/private/db/MemoryDB.js +335 -0
  92. package/lib/private/db/PK.d.ts +3 -0
  93. package/lib/private/db/PK.js +2 -0
  94. package/lib/private/{database → db}/Query.d.ts +2 -2
  95. package/lib/private/{database → db}/QueryBuilder.d.ts +2 -3
  96. package/lib/private/db/ReadArgs.d.ts +9 -0
  97. package/lib/private/db/ReadArgs.js +2 -0
  98. package/lib/private/db/ReadRelationsArgs.d.ts +6 -0
  99. package/lib/private/db/ReadRelationsArgs.js +2 -0
  100. package/lib/private/{database → db}/Schema.d.ts +1 -2
  101. package/lib/private/db/Sort.d.ts +4 -0
  102. package/lib/private/{database → db}/TypeMap.d.ts +2 -3
  103. package/lib/private/{database → db}/Types.d.ts +1 -1
  104. package/lib/private/db/With.d.ts +16 -0
  105. package/lib/private/db/With.js +2 -0
  106. package/lib/private/db/common.d.ts +25 -0
  107. package/lib/private/db/common.js +67 -0
  108. package/lib/private/db/errors.d.ts +104 -0
  109. package/lib/private/db/errors.js +237 -0
  110. package/lib/private/db/migrate/apply.d.ts +2 -0
  111. package/lib/private/db/migrate/apply.js +32 -0
  112. package/lib/private/db/migrate/bundle.d.ts +3 -0
  113. package/lib/private/db/migrate/bundle.js +22 -0
  114. package/lib/private/db/migrate/create.d.ts +2 -0
  115. package/lib/private/db/migrate/create.js +154 -0
  116. package/lib/private/db/migrate/index.d.ts +10 -0
  117. package/lib/private/db/migrate/index.js +6 -0
  118. package/lib/private/db/migrate/status.d.ts +2 -0
  119. package/lib/private/db/migrate/status.js +38 -0
  120. package/lib/private/db/migrate/store.d.ts +5 -0
  121. package/lib/private/db/migrate/store.js +33 -0
  122. package/lib/private/db/sql.d.ts +28 -0
  123. package/lib/private/db/sql.js +177 -0
  124. package/lib/private/db/storage.d.ts +3 -0
  125. package/lib/private/db/storage.js +3 -0
  126. package/lib/private/db/symbol.js +2 -0
  127. package/lib/private/db/test.d.ts +4 -0
  128. package/lib/private/db/test.js +1750 -0
  129. package/lib/private/errors.d.ts +88 -0
  130. package/lib/private/errors.js +211 -0
  131. package/lib/private/frontend.d.ts +72 -0
  132. package/lib/private/frontend.js +245 -0
  133. package/lib/private/hash.js +0 -1
  134. package/lib/private/i18n/Catalog.d.ts +5 -2
  135. package/lib/private/i18n/Formatter.js +21 -9
  136. package/lib/private/i18n/errors.d.ts +16 -0
  137. package/lib/private/i18n/errors.js +27 -0
  138. package/lib/private/i18n/format.d.ts +4 -0
  139. package/lib/private/i18n/format.js +95 -0
  140. package/lib/private/i18n/index/client.d.ts +9 -0
  141. package/lib/private/i18n/index/client.js +152 -0
  142. package/lib/private/i18n/index/server.d.ts +9 -0
  143. package/lib/private/i18n/index/server.js +57 -0
  144. package/lib/private/i18n/index/types.d.ts +33 -0
  145. package/lib/private/i18n/index/types.js +2 -0
  146. package/lib/private/i18n/locale.d.ts +9 -3
  147. package/lib/private/i18n/module.d.ts +3 -0
  148. package/lib/private/i18n/module.js +115 -0
  149. package/lib/private/i18n/ordinals.d.ts +1 -1
  150. package/lib/private/i18n/resolve.d.ts +7 -0
  151. package/lib/private/i18n/resolve.js +30 -0
  152. package/lib/private/i18n/schema.d.ts +10 -5
  153. package/lib/private/i18n/schema.js +6 -10
  154. package/lib/private/i18n/storage.d.ts +3 -0
  155. package/lib/private/i18n/storage.js +5 -0
  156. package/lib/private/i18n/validate.d.ts +2 -0
  157. package/lib/private/i18n/validate.js +19 -0
  158. package/lib/private/index.d.ts +14 -0
  159. package/lib/private/index.js +2 -0
  160. package/lib/private/log.d.ts +1 -0
  161. package/lib/private/log.js +15 -14
  162. package/lib/private/module/Hook.d.ts +1 -1
  163. package/lib/private/module/Next.d.ts +1 -1
  164. package/lib/private/module/Setup.d.ts +21 -0
  165. package/lib/private/module/Setup.js +2 -0
  166. package/lib/private/module/create.d.ts +16 -0
  167. package/lib/private/module/create.js +28 -0
  168. package/lib/private/orm/ExtractSchema.d.ts +9 -0
  169. package/lib/private/orm/ExtractSchema.js +2 -0
  170. package/lib/private/orm/ForeignKey.d.ts +14 -0
  171. package/lib/private/orm/ForeignKey.js +25 -0
  172. package/lib/private/orm/PrimaryKey.d.ts +17 -0
  173. package/lib/private/orm/PrimaryKey.js +38 -0
  174. package/lib/private/orm/StoreInput.d.ts +10 -0
  175. package/lib/private/orm/StoreInput.js +2 -0
  176. package/lib/private/orm/key.d.ts +8 -0
  177. package/lib/private/orm/key.js +8 -0
  178. package/lib/private/orm/parse.d.ts +13 -0
  179. package/lib/private/orm/parse.js +29 -0
  180. package/lib/private/orm/relation.d.ts +43 -0
  181. package/lib/private/orm/relation.js +26 -0
  182. package/lib/private/orm/store.d.ts +187 -0
  183. package/lib/private/orm/store.js +542 -0
  184. package/lib/private/paths.d.ts +2 -2
  185. package/lib/private/paths.js +3 -3
  186. package/lib/private/request/RequestBag.d.ts +5 -1
  187. package/lib/private/request/RequestBag.js +16 -9
  188. package/lib/private/request/RequestBody.d.ts +6 -11
  189. package/lib/private/request/RequestBody.js +51 -59
  190. package/lib/private/request/RequestContext.d.ts +12 -0
  191. package/lib/private/request/RequestContext.js +31 -0
  192. package/lib/private/request/RequestFacade.d.ts +11 -6
  193. package/lib/private/request/RequestPublic.d.ts +9 -0
  194. package/lib/private/request/RequestPublic.js +2 -0
  195. package/lib/private/request/RequestView.d.ts +11 -0
  196. package/lib/private/request/RequestView.js +3 -0
  197. package/lib/private/request/handle.d.ts +4 -0
  198. package/lib/private/request/handle.js +18 -0
  199. package/lib/private/request/parse.d.ts +2 -2
  200. package/lib/private/request/parse.js +55 -16
  201. package/lib/private/request/route.js +37 -22
  202. package/lib/private/request/router.d.ts +1 -1
  203. package/lib/private/request/router.js +22 -50
  204. package/lib/private/{database/symbol/wrap.d.ts → request/sContext.d.ts} +1 -1
  205. package/lib/private/request/sContext.js +2 -0
  206. package/lib/private/request/storage.d.ts +4 -0
  207. package/lib/private/request/storage.js +5 -0
  208. package/lib/private/response/ResponseFunction.d.ts +2 -3
  209. package/lib/private/response/ResponseLike.d.ts +1 -1
  210. package/lib/private/response/binary.d.ts +1 -1
  211. package/lib/private/response/binary.js +4 -3
  212. package/lib/private/response/error.d.ts +1 -1
  213. package/lib/private/response/error.js +1 -1
  214. package/lib/private/response/json.d.ts +1 -1
  215. package/lib/private/response/json.js +2 -2
  216. package/lib/private/response/redirect.d.ts +5 -5
  217. package/lib/private/response/redirect.js +9 -10
  218. package/lib/private/response/respond.js +16 -17
  219. package/lib/private/response/sse.d.ts +1 -1
  220. package/lib/private/response/sse.js +2 -2
  221. package/lib/private/response/text.d.ts +1 -1
  222. package/lib/private/response/text.js +3 -3
  223. package/lib/private/response/view.d.ts +3 -3
  224. package/lib/private/response/view.js +7 -14
  225. package/lib/private/response/ws.d.ts +1 -1
  226. package/lib/private/response.d.ts +1 -1
  227. package/lib/private/route/Handler.d.ts +1 -1
  228. package/lib/private/route/hook.d.ts +6 -0
  229. package/lib/private/route/hook.js +5 -0
  230. package/lib/private/route/router.d.ts +6 -2
  231. package/lib/private/route/router.js +32 -18
  232. package/lib/private/route/wrap.js +2 -2
  233. package/lib/private/serve/App.d.ts +11 -25
  234. package/lib/private/serve/App.js +138 -120
  235. package/lib/private/serve/Init.d.ts +5 -5
  236. package/lib/private/serve/dev-module.d.ts +2 -0
  237. package/lib/private/serve/dev-module.js +34 -0
  238. package/lib/private/serve/hook.d.ts +1 -2
  239. package/lib/private/serve/hook.js +2 -9
  240. package/lib/private/serve/index.d.ts +1 -1
  241. package/lib/private/serve/index.js +36 -2
  242. package/lib/private/server/TAG.d.ts +3 -0
  243. package/lib/private/server/TAG.js +2 -0
  244. package/lib/private/server/index.d.ts +5 -0
  245. package/lib/private/server/index.js +6 -0
  246. package/lib/private/session/Data.d.ts +1 -2
  247. package/lib/private/session/SessionHandle.d.ts +1 -1
  248. package/lib/private/session/SessionHandle.js +11 -9
  249. package/lib/private/session/index.d.ts +3 -4
  250. package/lib/private/session/module.d.ts +3 -0
  251. package/lib/private/session/module.js +114 -0
  252. package/lib/private/session/schema.d.ts +17 -9
  253. package/lib/private/session/schema.js +11 -6
  254. package/lib/private/session/storage.d.ts +1 -2
  255. package/lib/private/session/storage.js +2 -2
  256. package/lib/private/tags.js +2 -2
  257. package/lib/private/target/Manager.js +6 -12
  258. package/lib/private/target/Target.d.ts +1 -1
  259. package/lib/public/AppFacade.d.ts +2 -0
  260. package/lib/public/AppFacade.js +2 -0
  261. package/lib/public/build/presets.d.ts +2 -0
  262. package/lib/public/build/presets.js +2 -0
  263. package/lib/public/build/transform.d.ts +2 -0
  264. package/lib/public/build/transform.js +2 -0
  265. package/lib/public/client.d.ts +3 -0
  266. package/lib/public/client.js +2 -0
  267. package/lib/public/db/MemoryDB.d.ts +2 -0
  268. package/lib/public/db/MemoryDB.js +2 -0
  269. package/lib/public/db/errors.d.ts +2 -0
  270. package/lib/public/db/errors.js +2 -0
  271. package/lib/public/db/migrate.d.ts +2 -0
  272. package/lib/public/db/migrate.js +2 -0
  273. package/lib/public/db/sql.d.ts +2 -0
  274. package/lib/public/db/sql.js +2 -0
  275. package/lib/public/db/test.d.ts +2 -0
  276. package/lib/public/db/test.js +2 -0
  277. package/lib/public/db.d.ts +12 -0
  278. package/lib/public/db.js +2 -0
  279. package/lib/public/frontend.d.ts +3 -0
  280. package/lib/public/frontend.js +2 -0
  281. package/lib/public/index.d.ts +2 -0
  282. package/lib/public/index.js +2 -0
  283. package/lib/public/orm/key.d.ts +2 -0
  284. package/lib/public/orm/key.js +2 -0
  285. package/lib/public/orm/relation.d.ts +2 -0
  286. package/lib/public/orm/relation.js +2 -0
  287. package/lib/public/orm/store.d.ts +2 -0
  288. package/lib/public/orm/store.js +2 -0
  289. package/lib/public/request/server.d.ts +5 -0
  290. package/lib/public/request/server.js +7 -0
  291. package/lib/public/response.d.ts +22 -0
  292. package/lib/public/response.js +19 -0
  293. package/lib/public/route/hook.d.ts +2 -0
  294. package/lib/public/route/hook.js +2 -0
  295. package/lib/public/server.d.ts +3 -0
  296. package/lib/public/server.js +2 -0
  297. package/package.json +40 -30
  298. package/lib/private/AppError.d.ts +0 -4
  299. package/lib/private/AppError.js +0 -8
  300. package/lib/private/backend/Module.d.ts +0 -18
  301. package/lib/private/backend/Module.js +0 -22
  302. package/lib/private/backend/TAG.d.ts +0 -3
  303. package/lib/private/backend/TAG.js +0 -2
  304. package/lib/private/build/client/reload.d.ts +0 -7
  305. package/lib/private/build/client/reload.js +0 -6
  306. package/lib/private/build/server/plugin/database-default.d.ts +0 -4
  307. package/lib/private/build/server/plugin/database-default.js +0 -48
  308. package/lib/private/build/server/plugin/hot-reload.d.ts +0 -4
  309. package/lib/private/build/server/plugin/store-wrap.d.ts +0 -4
  310. package/lib/private/build/server/plugin/store-wrap.js +0 -33
  311. package/lib/private/client/spa/index.d.ts +0 -6
  312. package/lib/private/client/spa/index.js +0 -201
  313. package/lib/private/client/validate.d.ts +0 -3
  314. package/lib/private/client/validate.js +0 -54
  315. package/lib/private/database/As.d.ts +0 -7
  316. package/lib/private/database/Binds.d.ts +0 -4
  317. package/lib/private/database/Binds.js +0 -2
  318. package/lib/private/database/Changes.d.ts +0 -11
  319. package/lib/private/database/Changes.js +0 -2
  320. package/lib/private/database/ColumnTypes.d.ts +0 -11
  321. package/lib/private/database/ColumnTypes.js +0 -2
  322. package/lib/private/database/DataDict.d.ts +0 -5
  323. package/lib/private/database/DataKey.d.ts +0 -4
  324. package/lib/private/database/DataValue.d.ts +0 -5
  325. package/lib/private/database/Database.d.ts +0 -56
  326. package/lib/private/database/Database.js +0 -153
  327. package/lib/private/database/InMemoryDatabase.d.ts +0 -37
  328. package/lib/private/database/InMemoryDatabase.js +0 -181
  329. package/lib/private/database/Sort.d.ts +0 -4
  330. package/lib/private/database/Store.d.ts +0 -172
  331. package/lib/private/database/Store.js +0 -261
  332. package/lib/private/database/storage.d.ts +0 -4
  333. package/lib/private/database/storage.js +0 -3
  334. package/lib/private/database/symbol/wrap.js +0 -2
  335. package/lib/private/database/symbol.js +0 -2
  336. package/lib/private/database/test.d.ts +0 -4
  337. package/lib/private/database/test.js +0 -678
  338. package/lib/private/database/wrap.d.ts +0 -5
  339. package/lib/private/database/wrap.js +0 -5
  340. package/lib/private/fail.d.ts +0 -3
  341. package/lib/private/fail.js +0 -5
  342. package/lib/private/frontend/Module.d.ts +0 -62
  343. package/lib/private/frontend/Module.js +0 -250
  344. package/lib/private/frontend/Render.d.ts +0 -9
  345. package/lib/private/frontend/ServerView.d.ts +0 -5
  346. package/lib/private/i18n/Module.d.ts +0 -16
  347. package/lib/private/i18n/Module.js +0 -122
  348. package/lib/private/i18n/index.d.ts +0 -28
  349. package/lib/private/i18n/index.js +0 -236
  350. package/lib/private/module/BuildHook.d.ts +0 -5
  351. package/lib/private/module/BuildHook.js +0 -2
  352. package/lib/private/module/NextBuild.d.ts +0 -5
  353. package/lib/private/module/NextBuild.js +0 -2
  354. package/lib/private/module/NextServe.d.ts +0 -5
  355. package/lib/private/module/NextServe.js +0 -2
  356. package/lib/private/reducer.d.ts +0 -24
  357. package/lib/private/reducer.js +0 -10
  358. package/lib/private/route/guard.d.ts +0 -4
  359. package/lib/private/route/guard.js +0 -22
  360. package/lib/private/serve/module/Dev.d.ts +0 -11
  361. package/lib/private/serve/module/Dev.js +0 -28
  362. package/lib/private/serve/module/Handle.d.ts +0 -10
  363. package/lib/private/serve/module/Handle.js +0 -15
  364. package/lib/private/session/SessionModule.d.ts +0 -14
  365. package/lib/private/session/SessionModule.js +0 -122
  366. package/lib/private/wasm/API.d.ts +0 -7
  367. package/lib/private/wasm/API.js +0 -2
  368. package/lib/private/wasm/BufferViewSource.d.ts +0 -4
  369. package/lib/private/wasm/BufferViewSource.js +0 -2
  370. package/lib/private/wasm/Exports.d.ts +0 -23
  371. package/lib/private/wasm/Exports.js +0 -2
  372. package/lib/private/wasm/I32.d.ts +0 -5
  373. package/lib/private/wasm/I32.js +0 -2
  374. package/lib/private/wasm/I32_SIZE.d.ts +0 -3
  375. package/lib/private/wasm/I32_SIZE.js +0 -2
  376. package/lib/private/wasm/Instantiation.d.ts +0 -12
  377. package/lib/private/wasm/Instantiation.js +0 -2
  378. package/lib/private/wasm/Tagged.d.ts +0 -7
  379. package/lib/private/wasm/Tagged.js +0 -2
  380. package/lib/private/wasm/buffersize.d.ts +0 -2
  381. package/lib/private/wasm/buffersize.js +0 -5
  382. package/lib/private/wasm/decode-bytes.d.ts +0 -3
  383. package/lib/private/wasm/decode-bytes.js +0 -5
  384. package/lib/private/wasm/decode-json.d.ts +0 -7
  385. package/lib/private/wasm/decode-json.js +0 -11
  386. package/lib/private/wasm/decode-option.d.ts +0 -5
  387. package/lib/private/wasm/decode-option.js +0 -10
  388. package/lib/private/wasm/decode-response.d.ts +0 -19
  389. package/lib/private/wasm/decode-response.js +0 -90
  390. package/lib/private/wasm/decode-string.d.ts +0 -3
  391. package/lib/private/wasm/decode-string.js +0 -5
  392. package/lib/private/wasm/decode-websocket-close.d.ts +0 -5
  393. package/lib/private/wasm/decode-websocket-close.js +0 -6
  394. package/lib/private/wasm/decode-websocket-send.d.ts +0 -6
  395. package/lib/private/wasm/decode-websocket-send.js +0 -19
  396. package/lib/private/wasm/encode-buffer.d.ts +0 -3
  397. package/lib/private/wasm/encode-buffer.js +0 -6
  398. package/lib/private/wasm/encode-request.d.ts +0 -9
  399. package/lib/private/wasm/encode-request.js +0 -195
  400. package/lib/private/wasm/encode-session.d.ts +0 -3
  401. package/lib/private/wasm/encode-session.js +0 -25
  402. package/lib/private/wasm/encode-string-map.d.ts +0 -5
  403. package/lib/private/wasm/encode-string-map.js +0 -14
  404. package/lib/private/wasm/encode-string.d.ts +0 -13
  405. package/lib/private/wasm/encode-string.js +0 -17
  406. package/lib/private/wasm/encode-url.d.ts +0 -11
  407. package/lib/private/wasm/encode-url.js +0 -14
  408. package/lib/private/wasm/encode-websocket-close.d.ts +0 -2
  409. package/lib/private/wasm/encode-websocket-close.js +0 -7
  410. package/lib/private/wasm/encode-websocket-message.d.ts +0 -4
  411. package/lib/private/wasm/encode-websocket-message.js +0 -30
  412. package/lib/private/wasm/encode-websocket-open.d.ts +0 -2
  413. package/lib/private/wasm/encode-websocket-open.js +0 -7
  414. package/lib/private/wasm/filesize.d.ts +0 -2
  415. package/lib/private/wasm/filesize.js +0 -8
  416. package/lib/private/wasm/instantiate.d.ts +0 -37
  417. package/lib/private/wasm/instantiate.js +0 -408
  418. package/lib/private/wasm/open-websocket.d.ts +0 -4
  419. package/lib/private/wasm/open-websocket.js +0 -26
  420. package/lib/private/wasm/stringsize.d.ts +0 -3
  421. package/lib/private/wasm/stringsize.js +0 -5
  422. package/lib/private/wasm/urlsize.d.ts +0 -2
  423. package/lib/private/wasm/urlsize.js +0 -5
  424. package/lib/public/App.d.ts +0 -2
  425. package/lib/public/App.js +0 -2
  426. package/lib/public/AppError.d.ts +0 -2
  427. package/lib/public/AppError.js +0 -2
  428. package/lib/public/BuildApp.d.ts +0 -2
  429. package/lib/public/BuildApp.js +0 -2
  430. package/lib/public/BuildHook.d.ts +0 -2
  431. package/lib/public/BuildHook.js +0 -2
  432. package/lib/public/Database.d.ts +0 -2
  433. package/lib/public/Database.js +0 -2
  434. package/lib/public/Mode.d.ts +0 -2
  435. package/lib/public/Mode.js +0 -2
  436. package/lib/public/Module.d.ts +0 -2
  437. package/lib/public/Module.js +0 -2
  438. package/lib/public/Next.d.ts +0 -2
  439. package/lib/public/Next.js +0 -2
  440. package/lib/public/NextBuild.d.ts +0 -2
  441. package/lib/public/NextBuild.js +0 -2
  442. package/lib/public/NextHandle.d.ts +0 -2
  443. package/lib/public/NextHandle.js +0 -2
  444. package/lib/public/NextRoute.d.ts +0 -3
  445. package/lib/public/NextRoute.js +0 -2
  446. package/lib/public/NextServe.d.ts +0 -2
  447. package/lib/public/NextServe.js +0 -2
  448. package/lib/public/ServeApp.d.ts +0 -2
  449. package/lib/public/ServeApp.js +0 -2
  450. package/lib/public/Target.d.ts +0 -2
  451. package/lib/public/Target.js +0 -2
  452. package/lib/public/backend/Module.d.ts +0 -2
  453. package/lib/public/backend/Module.js +0 -2
  454. package/lib/public/backend/TAG.d.ts +0 -2
  455. package/lib/public/backend/TAG.js +0 -2
  456. package/lib/public/client/Data.d.ts +0 -2
  457. package/lib/public/client/Data.js +0 -2
  458. package/lib/public/client/ValidateInit.d.ts +0 -2
  459. package/lib/public/client/ValidateInit.js +0 -2
  460. package/lib/public/client/ValidateUpdater.d.ts +0 -2
  461. package/lib/public/client/ValidateUpdater.js +0 -2
  462. package/lib/public/client/ValidationError.d.ts +0 -2
  463. package/lib/public/client/ValidationError.js +0 -2
  464. package/lib/public/client/spa.d.ts +0 -2
  465. package/lib/public/client/spa.js +0 -2
  466. package/lib/public/client/toValidated.d.ts +0 -2
  467. package/lib/public/client/toValidated.js +0 -2
  468. package/lib/public/client/validate.d.ts +0 -2
  469. package/lib/public/client/validate.js +0 -2
  470. package/lib/public/database/As.d.ts +0 -2
  471. package/lib/public/database/As.js +0 -2
  472. package/lib/public/database/DataDict.d.ts +0 -2
  473. package/lib/public/database/DataDict.js +0 -2
  474. package/lib/public/database/InMemoryDatabase.d.ts +0 -2
  475. package/lib/public/database/InMemoryDatabase.js +0 -2
  476. package/lib/public/database/Sort.d.ts +0 -2
  477. package/lib/public/database/Sort.js +0 -2
  478. package/lib/public/database/Store.d.ts +0 -2
  479. package/lib/public/database/Store.js +0 -2
  480. package/lib/public/database/TypeMap.d.ts +0 -2
  481. package/lib/public/database/TypeMap.js +0 -2
  482. package/lib/public/database/Types.d.ts +0 -2
  483. package/lib/public/database/Types.js +0 -2
  484. package/lib/public/database/test.d.ts +0 -2
  485. package/lib/public/database/test.js +0 -2
  486. package/lib/public/database/wrap.d.ts +0 -2
  487. package/lib/public/database/wrap.js +0 -2
  488. package/lib/public/fail.d.ts +0 -2
  489. package/lib/public/fail.js +0 -2
  490. package/lib/public/frontend/Module.d.ts +0 -2
  491. package/lib/public/frontend/Module.js +0 -2
  492. package/lib/public/frontend/Publish.d.ts +0 -2
  493. package/lib/public/frontend/Publish.js +0 -2
  494. package/lib/public/frontend/Render.d.ts +0 -2
  495. package/lib/public/frontend/Render.js +0 -2
  496. package/lib/public/frontend/ViewResponse.d.ts +0 -2
  497. package/lib/public/frontend/ViewResponse.js +0 -2
  498. package/lib/public/request/RequestBody.d.ts +0 -2
  499. package/lib/public/request/RequestBody.js +0 -2
  500. package/lib/public/request/RequestFacade.d.ts +0 -2
  501. package/lib/public/request/RequestFacade.js +0 -2
  502. package/lib/public/request/Verb.d.ts +0 -2
  503. package/lib/public/request/Verb.js +0 -2
  504. package/lib/public/response/ResponseFunction.d.ts +0 -2
  505. package/lib/public/response/ResponseFunction.js +0 -2
  506. package/lib/public/response/ResponseLike.d.ts +0 -2
  507. package/lib/public/response/ResponseLike.js +0 -2
  508. package/lib/public/response/binary.d.ts +0 -2
  509. package/lib/public/response/binary.js +0 -2
  510. package/lib/public/response/error.d.ts +0 -2
  511. package/lib/public/response/error.js +0 -2
  512. package/lib/public/response/json.d.ts +0 -2
  513. package/lib/public/response/json.js +0 -2
  514. package/lib/public/response/redirect.d.ts +0 -2
  515. package/lib/public/response/redirect.js +0 -2
  516. package/lib/public/response/sse.d.ts +0 -2
  517. package/lib/public/response/sse.js +0 -2
  518. package/lib/public/response/text.d.ts +0 -2
  519. package/lib/public/response/text.js +0 -2
  520. package/lib/public/response/view.d.ts +0 -2
  521. package/lib/public/response/view.js +0 -2
  522. package/lib/public/response/ws.d.ts +0 -2
  523. package/lib/public/response/ws.js +0 -2
  524. package/lib/public/wasm/decode-json.d.ts +0 -5
  525. package/lib/public/wasm/decode-json.js +0 -3
  526. package/lib/public/wasm/decode-response.d.ts +0 -3
  527. package/lib/public/wasm/decode-response.js +0 -3
  528. package/lib/public/wasm/encode-request.d.ts +0 -3
  529. package/lib/public/wasm/encode-request.js +0 -3
  530. package/lib/public/wasm/encode-session.d.ts +0 -3
  531. package/lib/public/wasm/encode-session.js +0 -3
  532. package/lib/public/wasm/instantiate.d.ts +0 -4
  533. package/lib/public/wasm/instantiate.js +0 -3
  534. /package/lib/private/{frontend → client}/Publish.d.ts +0 -0
  535. /package/lib/private/{frontend → client}/Publish.js +0 -0
  536. /package/lib/private/{frontend → client}/Render.js +0 -0
  537. /package/lib/private/{frontend → client}/ServerData.js +0 -0
  538. /package/lib/private/{frontend → client}/ServerView.js +0 -0
  539. /package/lib/private/{frontend → client}/View.js +0 -0
  540. /package/lib/private/{frontend → client}/ViewOptions.js +0 -0
  541. /package/lib/private/{frontend → client}/ViewResponse.js +0 -0
  542. /package/lib/private/client/{spa/storage.d.ts → storage.d.ts} +0 -0
  543. /package/lib/private/client/{spa/storage.js → storage.js} +0 -0
  544. /package/lib/private/{database → db}/As.js +0 -0
  545. /package/lib/private/{database → db}/DataDict.js +0 -0
  546. /package/lib/private/{database → db}/DataKey.js +0 -0
  547. /package/lib/private/{database → db}/DataValue.js +0 -0
  548. /package/lib/private/{database → db}/Query.js +0 -0
  549. /package/lib/private/{database → db}/QueryBuilder.js +0 -0
  550. /package/lib/private/{database → db}/Schema.js +0 -0
  551. /package/lib/private/{database → db}/Sort.js +0 -0
  552. /package/lib/private/{database → db}/TypeMap.js +0 -0
  553. /package/lib/private/{database → db}/Types.js +0 -0
  554. /package/lib/private/{database → db}/primary.d.ts +0 -0
  555. /package/lib/private/{database → db}/primary.js +0 -0
  556. /package/lib/private/{database → db}/symbol.d.ts +0 -0
@@ -0,0 +1,1750 @@
1
+ import { Code } from "#db/errors";
2
+ import key from "#orm/key";
3
+ import relation from "#orm/relation";
4
+ import store from "#orm/store";
5
+ import test from "@rcompat/test";
6
+ import any from "@rcompat/test/any";
7
+ import p from "pema";
8
+ const BAD_WHERE = [
9
+ {
10
+ label: "reject array where value",
11
+ base: { name: ["Donald"] },
12
+ with: { title: ["foo a"] },
13
+ expected: Code.where_invalid_value,
14
+ },
15
+ {
16
+ label: "reject empty operator object",
17
+ base: { name: {} },
18
+ with: { title: {} },
19
+ expected: Code.operator_empty,
20
+ },
21
+ {
22
+ label: "reject undefined where value",
23
+ base: { age: undefined },
24
+ with: { title: undefined },
25
+ expected: Code.field_undefined,
26
+ },
27
+ {
28
+ label: "reject unknown operator",
29
+ base: { name: { $nope: "x" } },
30
+ with: { title: { $nope: "x" } },
31
+ expected: Code.operator_unknown,
32
+ },
33
+ ];
34
+ const BAD_SELECT = [
35
+ {
36
+ label: "reject empty select",
37
+ base: [],
38
+ with: [],
39
+ expected: Code.select_empty,
40
+ },
41
+ {
42
+ label: "reject unknown select column",
43
+ base: ["nope"],
44
+ with: ["nope"],
45
+ expected: Code.field_unknown,
46
+ },
47
+ {
48
+ label: "reject duplicate select fields",
49
+ base: ["id", "id"],
50
+ with: ["id", "id"],
51
+ expected: Code.field_duplicate,
52
+ },
53
+ ];
54
+ const BAD_SORT = [
55
+ {
56
+ label: "reject empty sort",
57
+ base: {},
58
+ with: {},
59
+ expected: Code.sort_empty,
60
+ },
61
+ {
62
+ label: "reject unknown sort column",
63
+ base: { nope: "asc" },
64
+ with: { nope: "asc" },
65
+ expected: Code.field_unknown,
66
+ },
67
+ {
68
+ label: "reject invalid direction",
69
+ base: { age: "ascending" },
70
+ with: { title: "ascending" },
71
+ expected: Code.sort_invalid_value,
72
+ },
73
+ {
74
+ label: "reject undefined direction",
75
+ base: { age: undefined },
76
+ with: { title: undefined },
77
+ expected: Code.sort_invalid_value,
78
+ },
79
+ ];
80
+ const BAD_WHERE_COLUMN = [
81
+ {
82
+ label: "reject unknown criteria column",
83
+ base: { nope: "x" },
84
+ with: { nope: "x" },
85
+ expected: Code.field_unknown,
86
+ },
87
+ ];
88
+ const USERS = {
89
+ ben: { age: 60, lastname: "Miller", name: "Ben" },
90
+ donald: { age: 30, lastname: "Duck", name: "Donald" },
91
+ jeremy: { age: 20, name: "Just Jeremy" },
92
+ paul: { age: 40, lastname: "Miller", name: "Paul" },
93
+ ryan: { age: 40, lastname: "Wilson", name: "Ryan" },
94
+ };
95
+ function pick(record, ...projection) {
96
+ return Object.fromEntries(Object.entries(record).filter(([k]) => projection.includes(k)));
97
+ }
98
+ async function throws(assert, code, fn) {
99
+ try {
100
+ await fn();
101
+ assert(false).true();
102
+ }
103
+ catch (error) {
104
+ assert(error.code).equals(code);
105
+ }
106
+ }
107
+ export default (db) => {
108
+ test.ended(() => db.close());
109
+ const Post = store({
110
+ name: "post",
111
+ db,
112
+ schema: {
113
+ id: key.primary(p.uuid),
114
+ title: p.string,
115
+ user_id: p.uint,
116
+ },
117
+ });
118
+ Post.update;
119
+ const User = store({
120
+ name: "user",
121
+ db,
122
+ schema: {
123
+ id: key.primary(p.uuid),
124
+ age: p.u8.optional(),
125
+ lastname: p.string.optional(),
126
+ name: p.string.default("Donald"),
127
+ },
128
+ });
129
+ const UserN = store({
130
+ name: "user_n",
131
+ db,
132
+ schema: {
133
+ id: key.primary(p.u32),
134
+ age: p.u8.optional(),
135
+ lastname: p.string.optional(),
136
+ name: p.string.default("Donald"),
137
+ },
138
+ });
139
+ const UserB = store({
140
+ name: "user_b",
141
+ db,
142
+ schema: {
143
+ id: key.primary(p.u128),
144
+ age: p.u8.optional(),
145
+ lastname: p.string.optional(),
146
+ name: p.string.default("Donald"),
147
+ },
148
+ });
149
+ const USER_STORES = [User, UserN, UserB];
150
+ const Type = store({
151
+ name: "type",
152
+ db,
153
+ schema: {
154
+ id: key.primary(p.uuid),
155
+ boolean: p.boolean.optional(),
156
+ date: p.date.optional(),
157
+ f32: p.f32.optional(),
158
+ f64: p.f64.optional(),
159
+ i128: p.i128.optional(),
160
+ i16: p.i16.optional(),
161
+ i32: p.i32.optional(),
162
+ i64: p.i64.optional(),
163
+ i8: p.i8.optional(),
164
+ string: p.string.optional(),
165
+ u128: p.u128.optional(),
166
+ u16: p.u16.optional(),
167
+ u32: p.u32.optional(),
168
+ u64: p.u64.optional(),
169
+ u8: p.u8.optional(),
170
+ json: p.json().optional(),
171
+ },
172
+ });
173
+ // this stresses identifier quoting in CREATE/INSERT/SELECT/UPDATE/DELETE
174
+ const Reserved = store({
175
+ // deliberately reserved-like table name
176
+ name: "select",
177
+ db,
178
+ schema: {
179
+ id: key.primary(p.uuid),
180
+ // deliberately reserved-looking column name
181
+ order: p.u8.optional(),
182
+ name: p.string,
183
+ },
184
+ });
185
+ const AuthorSchema = {
186
+ id: key.primary(p.uuid),
187
+ name: p.string,
188
+ };
189
+ const ArticleSchema = {
190
+ id: key.primary(p.uuid),
191
+ title: p.string,
192
+ author_id: key.foreign(p.uuid),
193
+ };
194
+ const ProfileSchema = {
195
+ id: key.primary(p.uuid),
196
+ bio: p.string,
197
+ url: p.url.optional(),
198
+ author_id: key.foreign(p.uuid),
199
+ };
200
+ const Author = store({
201
+ db,
202
+ name: "author",
203
+ schema: AuthorSchema,
204
+ relations: {
205
+ articles: relation.many(ArticleSchema, "author_id"),
206
+ profile: relation.one(ProfileSchema, "author_id"),
207
+ },
208
+ });
209
+ /*const AuthorWithJSON = store({
210
+ name: "author_json",
211
+ db,
212
+ schema: {
213
+ id: key.primary(p.uuid),
214
+ name: p.string,
215
+ meta: p.json(p({ views: p.u32, tags: p.array(p.string) })),
216
+ notes: p.json(), // untyped
217
+ },
218
+ });*/
219
+ const Article = store({
220
+ db,
221
+ name: "article",
222
+ schema: ArticleSchema,
223
+ relations: {
224
+ author: relation.one(AuthorSchema, "author_id", { reverse: true }),
225
+ },
226
+ });
227
+ const Profile = store({
228
+ db,
229
+ name: "profile",
230
+ schema: ProfileSchema,
231
+ relations: {
232
+ author: relation.one(AuthorSchema, "author_id", { reverse: true }),
233
+ },
234
+ });
235
+ function $store(label, s, body) {
236
+ test.case(label, async (assert) => {
237
+ await s.table.create();
238
+ try {
239
+ await body(assert);
240
+ }
241
+ finally {
242
+ await s.table.delete();
243
+ }
244
+ });
245
+ }
246
+ function $user(label, body) {
247
+ test.case(label, async (assert) => {
248
+ for (const S of USER_STORES)
249
+ await S.table.create();
250
+ try {
251
+ for (const u of Object.values(USERS)) {
252
+ for (const S of USER_STORES)
253
+ await S.insert(u);
254
+ }
255
+ await body(assert);
256
+ }
257
+ finally {
258
+ for (const S of USER_STORES)
259
+ await S.table.delete();
260
+ }
261
+ });
262
+ }
263
+ function $user$(label, body) {
264
+ $user(`security: ${label}`, body);
265
+ }
266
+ function $type(label, body) {
267
+ $store(`type: ${label}`, Type, body);
268
+ }
269
+ function $rel(label, body) {
270
+ test.case(`relation: ${label}`, async (assert) => {
271
+ await Author.table.create();
272
+ await Article.table.create();
273
+ await Profile.table.create();
274
+ try {
275
+ const john = await Author.insert({ name: "John" });
276
+ const bob = await Author.insert({ name: "Bob" });
277
+ const ned = await Author.insert({ name: "Ned" });
278
+ const jid = john.id;
279
+ const bid = bob.id;
280
+ const nid = ned.id;
281
+ await Article.insert({ title: "John First Post", author_id: jid });
282
+ await Article.insert({ title: "John Second Post", author_id: jid });
283
+ await Article.insert({ title: "Bob Only Post", author_id: bid });
284
+ await Profile.insert({
285
+ bio: "John is a writer",
286
+ url: new URL("https://example.com/john"),
287
+ author_id: jid,
288
+ });
289
+ const ts = ["foo a", "foo c", "foo d", "foo e", "foo f", "foo g"];
290
+ for (const title of ts)
291
+ await Article.insert({ title, author_id: jid });
292
+ await Article.insert({ title: "bar x", author_id: jid });
293
+ await Article.insert({ title: "foo b", author_id: bid });
294
+ await Article.insert({ title: "bar y", author_id: bid });
295
+ await Article.insert({ title: "bar z", author_id: nid });
296
+ await body(assert);
297
+ }
298
+ finally {
299
+ await Profile.table.delete();
300
+ await Article.table.delete();
301
+ await Author.table.delete();
302
+ }
303
+ });
304
+ }
305
+ function $rel$(label, body) {
306
+ $rel(`security: ${label}`, body);
307
+ }
308
+ function security_pair(base_prefix, label, mk, run_base, run_with, expected) {
309
+ const run = throws;
310
+ $user$(`${base_prefix}: ${label}`, async (assert) => {
311
+ const { base } = mk();
312
+ await run(assert, expected, () => run_base(base));
313
+ });
314
+ $rel$(`with: ${base_prefix}: ${label}`, async (assert) => {
315
+ const { with: w } = mk();
316
+ await run(assert, expected, () => run_with(w));
317
+ });
318
+ }
319
+ function bad_where(label, mk, expected) {
320
+ return security_pair("where", label, mk, base => User.find({ where: base }), w => Author.find({ with: { articles: { where: w } } }), expected);
321
+ }
322
+ function bad_select(label, mk, expected) {
323
+ return security_pair("find", label, mk, base => User.find({ select: base }), w => Author.find({ with: { articles: { select: w } } }), expected);
324
+ }
325
+ function bad_sort(label, mk, expected) {
326
+ return security_pair("find", label, mk, base => User.find({ sort: base }), w => Author.find({ with: { articles: { sort: w } } }), expected);
327
+ }
328
+ $store("insert", User, async (assert) => {
329
+ const donald = await User.insert({ age: 30, name: "Donald" });
330
+ assert(donald).type();
331
+ assert(await User.has(donald.id)).true();
332
+ const ryan = await User.insert({ age: 40, name: "Ryan" });
333
+ assert(await User.has(donald.id)).true();
334
+ assert(await User.has(ryan.id)).true();
335
+ });
336
+ $store("insert: primary key is optional (string)", User, async (assert) => {
337
+ const user = await User.insert({ name: "Test" });
338
+ assert(user.id).type();
339
+ assert(await User.has(user.id)).true();
340
+ });
341
+ $store("insert: primary key is optional (number)", UserN, async (assert) => {
342
+ const user = await UserN.insert({ name: "Test" });
343
+ assert(user.id).type();
344
+ assert(await UserN.has(user.id)).true();
345
+ });
346
+ $store("insert: primary key is optional (bigint)", UserB, async (assert) => {
347
+ const user = await UserB.insert({ name: "Test" });
348
+ assert(user.id).type();
349
+ assert(await UserB.has(user.id)).true();
350
+ });
351
+ const ManualUser = store({
352
+ name: "manual_user",
353
+ db,
354
+ schema: {
355
+ id: key.primary(p.uuid, { generate: false }),
356
+ name: p.string,
357
+ },
358
+ });
359
+ $store("insert: generate=false requires PK", ManualUser, async (assert) => {
360
+ await throws(assert, Code.pk_required, () => {
361
+ return ManualUser.insert({ name: "Test" });
362
+ });
363
+ });
364
+ $store("insert: generate=false accepts provided PK", ManualUser, async (assert) => {
365
+ const id = "4d0996db-bda9-4f95-ad7c-7075b10d4ba6";
366
+ const user = await ManualUser.insert({ id, name: "Test" });
367
+ assert(user.id).equals(id);
368
+ assert(await ManualUser.has(id)).true();
369
+ });
370
+ $store("insert: defaults apply", User, async (assert) => {
371
+ const u = await User.insert({});
372
+ assert(u.name).equals("Donald");
373
+ });
374
+ $store("insert: reject null values", User, async (assert) => {
375
+ await throws(assert, Code.null_not_allowed, () => {
376
+ // lastname is optional, but null is forbidden on insert
377
+ return User.insert({ name: "Nullman", lastname: null });
378
+ });
379
+ });
380
+ $user("find: empty object equals no options", async (assert) => {
381
+ const a = await User.find();
382
+ const b = await User.find({});
383
+ assert(a.length).equals(b.length);
384
+ });
385
+ $user("find: $like: handles regex metacharacters", async (assert) => {
386
+ await User.insert({ name: "A[1]" });
387
+ const got = await User.find({
388
+ where: { name: { $like: "A[1]" } },
389
+ select: ["name"],
390
+ });
391
+ assert(got.length).equals(1);
392
+ assert(got[0].name).equals("A[1]");
393
+ });
394
+ $user("find: types", async (assert) => {
395
+ const where = { name: "Ryan" };
396
+ assert(await User.find({ where })).type();
397
+ assert(await UserN.find({ where })).type();
398
+ assert(await UserB.find({ where })).type();
399
+ });
400
+ $user("find: select narrows type", async (assert) => {
401
+ const select = ["id", "name"];
402
+ const users = await User.find({ select });
403
+ assert(users).type();
404
+ const users_n = await UserN.find({ select });
405
+ assert(users_n).type();
406
+ const users_b = await UserB.find({ select });
407
+ assert(users_b).type();
408
+ });
409
+ $user("find: basic query", async (assert) => {
410
+ const result = await User.find({ where: { name: "Ryan" } });
411
+ assert(result.length).equals(1);
412
+ });
413
+ $user("find: sorting by multiple fields", async (assert) => {
414
+ // sorting by multiple fields: age descending, then Lastname ascending
415
+ const sorted = await User.find({
416
+ select: ["age", "name"],
417
+ sort: { age: "desc", lastname: "asc" },
418
+ });
419
+ assert(sorted.length).equals(5);
420
+ ["ben", "paul", "ryan", "donald", "jeremy"].forEach((user, i) => {
421
+ assert(sorted[i]).equals(pick(USERS[user], "name", "age"));
422
+ });
423
+ const descending = await User.find({
424
+ select: ["age", "name"],
425
+ sort: { age: "desc", lastname: "desc" },
426
+ });
427
+ const descended = ["ben", "ryan", "paul", "donald", "jeremy"];
428
+ descended.forEach((user, i) => {
429
+ assert(descending[i]).equals(pick(USERS[user], "name", "age"));
430
+ });
431
+ });
432
+ $user("find: sorting ascending and descending", async (assert) => {
433
+ const ascending = await User.find({
434
+ select: ["age", "name"],
435
+ sort: { age: "asc" },
436
+ limit: 2,
437
+ });
438
+ const ascended = ["jeremy", "donald"];
439
+ ascended.forEach((user, i) => {
440
+ assert(ascending[i]).equals(pick(USERS[user], "name", "age"));
441
+ });
442
+ const descending = await User.find({
443
+ select: ["age", "name"],
444
+ sort: { age: "desc" },
445
+ limit: 1,
446
+ });
447
+ assert(descending[0]).equals(pick(USERS.ben, "name", "age"));
448
+ });
449
+ $user("find: null criteria uses IS NULL semantics", async (assert) => {
450
+ // inserted fixtures include Jeremy without a lastname (NULL in DB)
451
+ // querying with { lastname: null } should find him
452
+ const rows = await User.find({
453
+ select: ["name", "lastname"],
454
+ where: { lastname: null },
455
+ sort: { name: "asc" },
456
+ });
457
+ assert(rows.length).equals(1);
458
+ if (rows.length > 0) {
459
+ assert(rows[0].name).equals("Just Jeremy");
460
+ }
461
+ });
462
+ $user("find: $like operator for strings", async (assert) => {
463
+ const prefix = await User.find({ where: { name: { $like: "J%" } } });
464
+ assert(prefix.length).equals(1);
465
+ if (prefix.length > 0)
466
+ assert(prefix[0].name).equals("Just Jeremy");
467
+ const suffix = await User.find({ where: { lastname: { $like: "%er" } } });
468
+ assert(suffix.length).equals(2);
469
+ const lastnames = suffix.map(u => u.lastname).sort();
470
+ assert(lastnames).equals(["Miller", "Miller"]);
471
+ const contains = await User.find({ where: { name: { $like: "%on%" } } });
472
+ assert(contains.length).equals(1);
473
+ if (contains.length > 0)
474
+ assert(contains[0].name).equals("Donald");
475
+ const exact = await User.find({ where: { name: { $like: "Ryan" } } });
476
+ assert(exact.length).equals(1);
477
+ if (exact.length > 0)
478
+ assert(exact[0].name).equals("Ryan");
479
+ const none = await User.find({ where: { name: { $like: "xyz%" } } });
480
+ assert(none.length).equals(0);
481
+ });
482
+ $user("find: $like with null/undefined fields", async (assert) => {
483
+ // Jeremy has no lastname (null), should not match ANY $like patterns
484
+ const results = await User.find({
485
+ where: { lastname: { $like: "%ll%" } },
486
+ });
487
+ assert(results.length).equals(2); // Ben, Paul
488
+ const names = results.map(u => u.name).sort();
489
+ assert(names).equals(["Ben", "Paul"]);
490
+ });
491
+ $user("find: $like: reject non-string types", async (assert) => {
492
+ await throws(assert, Code.operator_unknown, () => {
493
+ // age is u8, should not accept $like
494
+ return User.find({ where: { age: { $like: "30%" } } });
495
+ });
496
+ });
497
+ $user("update: single record", async (assert) => {
498
+ const [donald] = (await User.find({ where: { name: "Donald" } }));
499
+ await User.update(donald.id, { set: { age: 35 } });
500
+ const [updated] = (await User.find({ where: { name: "Donald" } }));
501
+ assert(updated.age).equals(35);
502
+ assert(updated).equals({ ...USERS.donald, age: 35, id: donald.id });
503
+ });
504
+ $user("update: unset fields", async (assert) => {
505
+ const [donald] = (await User.find({ where: { name: "Donald" } }));
506
+ assert(donald.age).equals(30);
507
+ await User.update(donald.id, { set: { age: null } });
508
+ const [updated] = (await User.find({ where: { name: "Donald" } }));
509
+ assert(updated.age).undefined();
510
+ const [paul] = (await User.find({ where: { name: "Paul" } }));
511
+ await User.update(paul.id, { set: { age: null, lastname: null } });
512
+ const [updated_paul] = (await User.find({ where: { name: "Paul" } }));
513
+ assert(updated_paul).equals({ id: updated_paul.id, name: "Paul" });
514
+ });
515
+ $user("update: cannot unset required fields", async (assert) => {
516
+ const [donald] = await User.find({ where: { name: "Donald" } });
517
+ await throws(assert, Code.null_not_allowed, () => {
518
+ // name is required (non-nullable) -> cannot be unset
519
+ return User.update(donald.id, { set: { name: null } });
520
+ });
521
+ });
522
+ $user("update: cannot unset required fields (multi-update)", async (assert) => {
523
+ await throws(assert, Code.null_not_allowed, () => {
524
+ return User.update({
525
+ where: { name: { $like: "D%" } },
526
+ set: { name: null },
527
+ });
528
+ });
529
+ });
530
+ $user("update: multiple records", async (assert) => {
531
+ const n_updated = await User.update({
532
+ where: { age: 40 },
533
+ set: { age: 45 },
534
+ });
535
+ assert(n_updated).equals(2);
536
+ const updated = await User.find({ where: { age: 45 } });
537
+ assert(updated.length).equals(2);
538
+ });
539
+ $user("update: where and changeset share a column", async (assert) => {
540
+ // Donald has age 30; update age using criteria on the same column
541
+ const n = await User.update({ where: { age: 30 }, set: { age: 31 } });
542
+ assert(n).equals(1);
543
+ assert(await User.count({ where: { age: 30 } })).equals(0);
544
+ assert(await User.count({ where: { age: 31 } })).equals(1);
545
+ const [donald] = await User.find({ where: { name: "Donald" } });
546
+ assert(donald.age).equals(31);
547
+ });
548
+ $rel("count: no with", async (assert) => {
549
+ await throws(assert, Code.count_with_invalid, async () => {
550
+ await Author.count({ with: { articles: true } });
551
+ });
552
+ });
553
+ $user("update: update all", async (assert) => {
554
+ await User.update({ set: { age: 99 } });
555
+ const users = await User.find();
556
+ for (const user of users)
557
+ assert(user.age).equals(99);
558
+ });
559
+ $user("update: $like criteria", async (assert) => {
560
+ // update all users whose names start with "J"
561
+ const updated = await User.update({
562
+ where: { name: { $like: "J%" } },
563
+ set: { age: 25 },
564
+ });
565
+ assert(updated).equals(1);
566
+ const jeremy = await User.find({ where: { name: "Just Jeremy" } });
567
+ assert(jeremy[0].age).equals(25);
568
+ });
569
+ $user("delete: single record", async (assert) => {
570
+ const [donald] = await User.find({ where: { name: "Donald" } });
571
+ await User.delete(donald.id);
572
+ const deleted = await User.find({ where: { name: "Donald" } });
573
+ assert(deleted.length).equals(0);
574
+ });
575
+ $user("delete: multiple records", async (assert) => {
576
+ const n = await User.delete({ where: { age: 40 } });
577
+ assert(n).equals(2);
578
+ const remaining = await User.find({ sort: { age: "asc" } });
579
+ assert(remaining.length).equals(3);
580
+ assert(remaining[0].name).equals("Just Jeremy");
581
+ });
582
+ $user("delete: $like criteria", async (assert) => {
583
+ // delete all users with "Miller" lastname
584
+ const deleted = await User.delete({
585
+ where: { lastname: { $like: "%Miller%" } },
586
+ });
587
+ assert(deleted).equals(2);
588
+ const remaining = await User.find();
589
+ assert(remaining.length).equals(3);
590
+ const remainingNames = remaining.map(u => u.name).sort();
591
+ assert(remainingNames).equals(["Donald", "Just Jeremy", "Ryan"]);
592
+ });
593
+ $user("count", async (assert) => {
594
+ assert(await User.count()).equals(5);
595
+ assert(await User.count({ where: { name: "Ryan" } })).equals(1);
596
+ assert(await User.count({ where: { age: 40 } })).equals(2);
597
+ assert(await User.count({ where: { age: 30 } })).equals(1);
598
+ assert(await User.count({ where: { age: 35 } })).equals(0);
599
+ });
600
+ $user("count: $like operator", async (assert) => {
601
+ assert(await User.count({ where: { name: { $like: "J%" } } })).equals(1);
602
+ assert(await User.count({ where: { lastname: { $like: "%er" } } }))
603
+ .equals(2);
604
+ assert(await User.count({ where: { name: { $like: "%xyz%" } } }))
605
+ .equals(0);
606
+ });
607
+ $user("has", async (assert) => {
608
+ const [donald] = await User.find({ where: { name: "Donald" } });
609
+ assert(await User.has(donald.id)).true();
610
+ await User.delete(donald.id);
611
+ assert(await User.has(donald.id)).false();
612
+ });
613
+ $type("boolean", async (assert) => {
614
+ const t = await Type.insert({ boolean: true });
615
+ assert(t.boolean).equals(true);
616
+ assert((await Type.get(t.id)).boolean).equals(true);
617
+ await Type.update(t.id, { set: { boolean: false } });
618
+ assert((await Type.get(t.id)).boolean).equals(false);
619
+ });
620
+ $type("string", async (assert) => {
621
+ const t = await Type.insert({ string: "foo" });
622
+ assert(t.string).equals("foo");
623
+ assert((await Type.get(t.id)).string).equals("foo");
624
+ await Type.update(t.id, { set: { string: "bar" } });
625
+ assert((await Type.get(t.id)).string).equals("bar");
626
+ });
627
+ $type("date", async (assert) => {
628
+ const now = new Date();
629
+ const t = await Type.insert({ date: now });
630
+ assert(t.date?.getTime()).equals(now.getTime());
631
+ assert((await Type.get(t.id)).date?.getTime()).equals(now.getTime());
632
+ const next = new Date();
633
+ await Type.update(t.id, { set: { date: next } });
634
+ assert((await Type.get(t.id)).date).equals(next);
635
+ });
636
+ $type("f32", async (assert) => {
637
+ const t = await Type.insert({ f32: 1.5 });
638
+ assert(t.f32).equals(1.5);
639
+ assert((await Type.get(t.id)).f32).equals(1.5);
640
+ await Type.update(t.id, { set: { f32: 123456.75 } });
641
+ assert((await Type.get(t.id)).f32).equals(123456.75);
642
+ });
643
+ $type("f64", async (assert) => {
644
+ const f1 = 123456.78901;
645
+ const t = await Type.insert({ f64: f1 });
646
+ assert(t.f64).equals(f1);
647
+ assert((await Type.get(t.id)).f64).equals(f1);
648
+ await Type.update(t.id, { set: { f32: 1.5 } });
649
+ assert((await Type.get(t.id)).f32).equals(1.5);
650
+ });
651
+ [8, 16, 32].forEach(n => {
652
+ $type(`i${n}`, async (assert) => {
653
+ const k = `i${n}`;
654
+ // lower bound
655
+ const lb = -(2 ** (n - 1));
656
+ const t = await Type.insert({ [k]: lb });
657
+ assert(t[any(k)]).equals(lb);
658
+ assert((await Type.get(t.id))[any(k)]).equals(lb);
659
+ // upper bound
660
+ const ub = 2 ** (n - 1) - 1;
661
+ await Type.update(t.id, { set: { [k]: ub } });
662
+ assert((await Type.get(t.id))[any(k)]).equals(ub);
663
+ });
664
+ $type(`u${n}`, async (assert) => {
665
+ const k = `u${n}`;
666
+ // lower bound
667
+ const lb = 0;
668
+ const t = await Type.insert({ [k]: lb });
669
+ assert(t[any(k)]).equals(lb);
670
+ assert((await Type.get(t.id))[any(k)]).equals(lb);
671
+ // upper bound
672
+ const ub = 2 ** n - 1;
673
+ await Type.update(t.id, { set: { [k]: ub } });
674
+ assert((await Type.get(t.id))[any(k)]).equals(ub);
675
+ });
676
+ });
677
+ [64n, 128n].forEach(i => {
678
+ $type(`i${i}`, async (assert) => {
679
+ const k = `i${i}`;
680
+ // lower bound
681
+ const lb = -(2n ** (i - 1n));
682
+ const t = await Type.insert({ [k]: lb });
683
+ assert(t[any(k)]).equals(lb);
684
+ assert((await Type.get(t.id))[any(k)]).equals(lb);
685
+ // upper bound
686
+ const ub = 2n ** (i - 1n) - 1n;
687
+ const tu = await Type.insert({ [k]: ub });
688
+ assert(tu[any(k)]).equals(ub);
689
+ assert((await Type.get(tu.id))[any(k)]).equals(ub);
690
+ });
691
+ $type(`u${i}`, async (assert) => {
692
+ const k = `u${i}`;
693
+ // lower bound
694
+ const lb = 0n;
695
+ const t = await Type.insert({ [k]: lb });
696
+ assert(t[any(k)]).equals(lb);
697
+ assert((await Type.get(t.id))[any(k)]).equals(lb);
698
+ // upper bound
699
+ const ub = 2n ** i - 1n;
700
+ const tu = await Type.insert({ [k]: ub });
701
+ assert(tu[any(k)]).equals(ub);
702
+ assert((await Type.get(tu.id))[any(k)]).equals(ub);
703
+ });
704
+ });
705
+ $user$("find: reject non-array select", async (assert) => {
706
+ await throws(assert, Code.select_invalid, () => {
707
+ return User.find({ select: {} });
708
+ });
709
+ });
710
+ $user$("find: projection limits fields", async (assert) => {
711
+ const records = await User.find({ select: ["id", "name"] });
712
+ assert(records.length).equals(5);
713
+ for (const r of records) {
714
+ // only id + name must be present
715
+ assert(Object.keys(r).toSorted()).equals(["id", "name"].toSorted());
716
+ }
717
+ });
718
+ $user$("update: reject unknown field on set", async (assert) => {
719
+ const [donald] = await User.find({ where: { name: "Donald" } });
720
+ await throws(assert, Code.field_unknown, () => {
721
+ return User.update(donald.id, { set: { nope: 1 } });
722
+ });
723
+ });
724
+ $user$("update: reject empty set object", async (assert) => {
725
+ const [donald] = await User.find({ where: { name: "Donald" } });
726
+ await throws(assert, Code.set_empty, () => {
727
+ return User.update(donald.id, { set: {} });
728
+ });
729
+ });
730
+ $user$("update: reject updating primary key", async (assert) => {
731
+ const [donald] = await User.find({ where: { name: "Donald" } });
732
+ await throws(assert, Code.pk_immutable, () => {
733
+ return User.update(donald.id, { set: { id: "nope" } });
734
+ });
735
+ });
736
+ $user$("update: reject missing set", async (assert) => {
737
+ await throws(assert, Code.set_empty, () => {
738
+ return User.update({ where: { name: "Donald" } });
739
+ });
740
+ });
741
+ $user$("delete: reject missing where", async (assert) => {
742
+ await throws(assert, Code.where_required, () => {
743
+ return User.delete({});
744
+ });
745
+ });
746
+ $user$("inject invalid identifier (where)", async (assert) => {
747
+ // attempted injection via bogus key
748
+ await throws(assert, Code.identifier_invalid, () => {
749
+ return User.find({ where: { "name; DROP TABLE user;": "x" } });
750
+ });
751
+ });
752
+ $user$("inject invalid identifier (select)", async (assert) => {
753
+ await throws(assert, Code.identifier_invalid, () => {
754
+ return User.find({ select: ["name; DROP TABLE user;"] });
755
+ });
756
+ });
757
+ $user$("value safety: does not interpolate values", async (assert) => {
758
+ const evil = "x' ; DROP TABLE user; --";
759
+ // should just behave like a normal string compare (0 matches), not explode
760
+ const rows = await User.find({ where: { name: evil } });
761
+ assert(rows.length).equals(0);
762
+ // table should still exist / be queryable
763
+ assert(await User.count()).equals(5);
764
+ });
765
+ $user$("update respects unset / binding map", async (assert) => {
766
+ const [paul] = await User.find({ where: { name: "Paul" } });
767
+ // allowed, lastname is optional
768
+ await User.update(paul.id, { set: { lastname: null } });
769
+ const [after] = await User.find({
770
+ select: ["id", "name", "lastname"],
771
+ where: { id: paul.id },
772
+ });
773
+ assert(after.lastname).undefined();
774
+ });
775
+ $user$("count: reject unknown where column", async (assert) => {
776
+ await throws(assert, Code.field_unknown, () => {
777
+ return User.count({ where: { nope: 1 } });
778
+ });
779
+ });
780
+ $user$("$like: reject unknown field", async (assert) => {
781
+ await throws(assert, Code.field_unknown, () => {
782
+ return User.find({ where: { unknown: { $like: "test%" } } });
783
+ });
784
+ });
785
+ $user$("insert: reject unknown column", async (assert) => {
786
+ await throws(assert, Code.field_unknown, () => {
787
+ return User.insert({ name: "X", nope: 1 });
788
+ });
789
+ });
790
+ $user$("update: reject unknown where column", async (assert) => {
791
+ await throws(assert, Code.field_unknown, () => User.update({
792
+ where: { nope: 1 },
793
+ set: { age: 1 },
794
+ }));
795
+ });
796
+ $user$("delete: reject unknown where column", async (assert) => {
797
+ await throws(assert, Code.field_unknown, () => User.delete({ where: { nope: 1 } }));
798
+ });
799
+ $user$("number operators: reject on non-number fields", async (assert) => {
800
+ await throws(assert, Code.operator_unknown, () => {
801
+ // name is string, should not accept $gte
802
+ return User.find({ where: { name: { $gte: 10 } } });
803
+ });
804
+ });
805
+ $user$("mixed operators: reject invalid combinations", async (assert) => {
806
+ await throws(assert, Code.operator_unknown, () => {
807
+ // can't mix string and number operators
808
+ return User.find({ where: { name: { $like: "test%", $gte: 5 } } });
809
+ });
810
+ });
811
+ $store("insert: reject null on required fields", User, async (assert) => {
812
+ await throws(assert, Code.null_not_allowed, () => {
813
+ // name is required, but passing null explicitly is still forbidden
814
+ return User.insert({ name: null });
815
+ });
816
+ });
817
+ $store("insert: omission stores NULL", User, async (assert) => {
818
+ const u = await User.insert({ name: "NoLast" });
819
+ const [got] = await User.find({
820
+ where: { id: u.id },
821
+ select: ["id", "name", "lastname"],
822
+ });
823
+ assert(got.id).equals(u.id);
824
+ assert(got.name).equals("NoLast");
825
+ // returned shape must not contain null
826
+ assert(got.lastname).undefined();
827
+ });
828
+ $store("insert: default fields can be omitted", User, async (assert) => {
829
+ // User has name: p.string.default("Donald")
830
+ // should be insertable without providing name
831
+ const user = await User.insert({ age: 30 });
832
+ assert(user.name).equals("Donald");
833
+ });
834
+ $store("reserved table / column names", Reserved, async (assert) => {
835
+ const a = await Reserved.insert({ name: "alpha", order: 1 });
836
+ const b = await Reserved.insert({ name: "beta", order: 2 });
837
+ const got = await Reserved.find({ where: { name: "alpha" } });
838
+ assert(got.length).equals(1);
839
+ assert(got[0]).equals({ id: a.id, name: "alpha", order: 1 });
840
+ // update using the reserved column
841
+ const n = await Reserved.update({
842
+ where: { name: "beta" },
843
+ set: { order: 9 },
844
+ });
845
+ assert(n).equals(1);
846
+ const [after] = await Reserved.find({ where: { id: b.id } });
847
+ assert(after.order).equals(9);
848
+ // and delete to complete the cycle
849
+ await Reserved.delete(a.id);
850
+ assert(await Reserved.has(a.id)).false();
851
+ });
852
+ $rel("get with one (reverse)", async (assert) => {
853
+ const articles = await Article.find();
854
+ const article = await Article.get(articles[0].id, {
855
+ with: {
856
+ author: true,
857
+ },
858
+ });
859
+ assert(article).type();
860
+ assert(article.author).not.null();
861
+ assert(article.author?.name).defined();
862
+ });
863
+ $rel("get with many", async (assert) => {
864
+ const [first] = await Author.find({ where: { name: "John" } });
865
+ const john = await Author.get(first.id, { with: { articles: true } });
866
+ assert(john.articles).type();
867
+ const titles = john.articles.map(a => a.title);
868
+ assert(titles.includes("John First Post")).true();
869
+ assert(titles.includes("John Second Post")).true();
870
+ });
871
+ $rel("get by id", async (assert) => {
872
+ const [first] = await Author.find({ where: { name: "John" } });
873
+ const john = await Author.get(first.id, { with: { profile: true } });
874
+ assert(john.profile).not.null();
875
+ assert(john.profile?.bio).equals("John is a writer");
876
+ });
877
+ $rel("get by id returns null when missing", async (assert) => {
878
+ const [first] = await Author.find({ where: { name: "Bob" } });
879
+ const bob = await Author.get(first.id, { with: { profile: true } });
880
+ assert(bob.profile).null();
881
+ });
882
+ $rel("find", async (assert) => {
883
+ const articles = await Article.find({
884
+ where: { title: { $like: "% Post" } },
885
+ with: { author: true },
886
+ sort: { title: "asc" },
887
+ });
888
+ assert(articles.length).equals(3);
889
+ for (const article of articles) {
890
+ assert(article.author).not.null();
891
+ assert(article.author?.name).defined();
892
+ }
893
+ });
894
+ $rel("try", async (assert) => {
895
+ const [first] = await Article.find();
896
+ const article = await Article.try(first.id, { with: { author: true } });
897
+ assert(article).defined();
898
+ assert(article?.author).not.null();
899
+ });
900
+ $rel("many returns empty array when no matches", async (assert) => {
901
+ // insert author with no articles
902
+ const lonely = await Author.insert({ name: "Lonely" });
903
+ const author = await Author.get(lonely.id, { with: { articles: true } });
904
+ assert(author.articles).type();
905
+ assert(author.articles.length).equals(0);
906
+ });
907
+ $rel("multiple relations in one query", async (assert) => {
908
+ const authors = await Author.find({ where: { name: "John" } });
909
+ const john = await Author.get(authors[0].id, {
910
+ with: {
911
+ articles: true,
912
+ profile: true,
913
+ },
914
+ });
915
+ const titles = john.articles.map(a => a.title);
916
+ assert(titles.includes("John First Post")).true();
917
+ assert(titles.includes("John Second Post")).true();
918
+ assert(john.profile).not.null();
919
+ assert(john.profile?.bio).equals("John is a writer");
920
+ });
921
+ $rel("no fields without 'with'", async (assert) => {
922
+ const articles = await Article.find();
923
+ const article = await Article.get(articles[0].id);
924
+ assert("author" in article).false();
925
+ });
926
+ $rel("find: complex relation subqueries", async (assert) => {
927
+ const SUBLIMIT = 5;
928
+ const authors = await Author.find({
929
+ select: ["id", "name"],
930
+ sort: { name: "asc" },
931
+ limit: 20,
932
+ with: {
933
+ articles: {
934
+ where: { title: { $like: "%foo%" } },
935
+ select: ["id", "title"],
936
+ sort: { title: "asc" },
937
+ limit: SUBLIMIT,
938
+ },
939
+ profile: true,
940
+ },
941
+ });
942
+ assert(authors).type();
943
+ // parents must *not* be filtered by relation where
944
+ const base = await Author.find({
945
+ select: ["id", "name"],
946
+ sort: { name: "asc" },
947
+ limit: 20,
948
+ });
949
+ assert(authors.map(r => r.name)).equals(base.map(r => r.name));
950
+ // base projection is respected (only id/name + relation keys)
951
+ for (const author of authors) {
952
+ assert(Object.keys(author).toSorted()).equals(["articles", "id", "name", "profile"].toSorted());
953
+ }
954
+ // per-parent relation correctness (filter + sort + limit)
955
+ for (const author of authors) {
956
+ // many => always array
957
+ assert(Array.isArray(author.articles)).true();
958
+ // projection + filter enforcement on returned relation rows
959
+ for (const article of author.articles) {
960
+ assert(Object.keys(article).toSorted()).equals(["id", "title"]
961
+ .toSorted());
962
+ assert(article.title.includes("foo")).true();
963
+ }
964
+ // relation sort asc by title
965
+ const titles = author.articles.map(a => a.title);
966
+ assert([...titles].toSorted()).equals(titles);
967
+ // relation limit is per-parent
968
+ assert(author.articles.length <= SUBLIMIT).true();
969
+ // compare against base query (same semantics, explicit per-parent)
970
+ const expected = await Article.find({
971
+ where: { author_id: author.id, title: { $like: "%foo%" } },
972
+ select: ["id", "title"],
973
+ sort: { title: "asc" },
974
+ });
975
+ const expected_titles = expected.slice(0, SUBLIMIT).map(a => a.title);
976
+ assert(titles).equals(expected_titles);
977
+ // profile: one => null or full record, FK must point back
978
+ if (author.profile !== null) {
979
+ assert(Object.keys(author.profile).toSorted()).equals(["author_id", "bio", "id", "url"].toSorted());
980
+ assert(author.profile.author_id).equals(author.id);
981
+ }
982
+ }
983
+ // ensure the inner limit was actually used (needs > SUBLIMIT matches)
984
+ let used = false;
985
+ for (const author of authors) {
986
+ const n_foo = await Article.count({
987
+ where: { author_id: author.id, title: { $like: "%foo%" } },
988
+ });
989
+ if (n_foo > SUBLIMIT)
990
+ used = true;
991
+ }
992
+ assert(used).true();
993
+ const john = authors.find(r => r.name === "John");
994
+ if (john !== undefined) {
995
+ assert(john.profile).not.null();
996
+ assert(john.profile?.bio).equals("John is a writer");
997
+ }
998
+ // parent with *zero* matching related rows must still be present,
999
+ // and the many-relation must be [] (not missing / not null)
1000
+ const ned = authors.find(a => a.name === "Ned");
1001
+ assert(ned).defined();
1002
+ assert(ned.articles).type();
1003
+ assert(ned.articles.length).equals(0);
1004
+ // and one-relation should be null when missing
1005
+ assert(ned.profile).null();
1006
+ });
1007
+ $rel("type: select narrows nested types", async (assert) => {
1008
+ const rows = await Author.find({
1009
+ select: ["id"],
1010
+ with: {
1011
+ profile: { select: ["bio"] },
1012
+ articles: { select: ["title"] },
1013
+ },
1014
+ });
1015
+ assert(rows).type();
1016
+ // runtime: selected base only has id (plus relation keys)
1017
+ for (const r of rows)
1018
+ assert("name" in r).false();
1019
+ });
1020
+ $rel$("with: reject unknown relation name", async (assert) => {
1021
+ await throws(assert, Code.relation_unknown, () => {
1022
+ return Author.find({ with: { nope: true } });
1023
+ });
1024
+ });
1025
+ $user("get/try: missing id", async (assert) => {
1026
+ const missing = "00000000-0000-4000-8000-000000000000";
1027
+ // const missing = `missing-${Date.now()}-${Math.random()}`;
1028
+ await throws(assert, Code.record_not_found, () => User.get(missing));
1029
+ assert(await User.try(missing)).undefined();
1030
+ });
1031
+ $rel("get/try: missing id (with relations)", async (assert) => {
1032
+ //const missing = `missing-${Date.now()}-${Math.random()}`;
1033
+ const missing = "00000000-0000-4000-8000-000000000000";
1034
+ await throws(assert, Code.record_not_found, () => Article.get(missing, { with: { author: true } }));
1035
+ assert(await Article.try(missing, { with: { author: true } })).undefined();
1036
+ });
1037
+ $rel("get/try: missing id (+ parent)", async (assert) => {
1038
+ const missing = "00000000-0000-4000-8000-000000000000";
1039
+ //const missing = `missing-${Date.now()}-${Math.random()}`;
1040
+ await throws(assert, Code.record_not_found, () => Author.get(missing, { with: { articles: true, profile: true } }));
1041
+ assert(await Author.try(missing, {
1042
+ with: { articles: true, profile: true },
1043
+ })).undefined();
1044
+ });
1045
+ $user("try: does not swallow invalid options", async (assert) => {
1046
+ const [u] = await User.find({ select: ["id"] });
1047
+ await throws(assert, Code.select_empty, () => {
1048
+ return User.try(u.id, { select: [] });
1049
+ });
1050
+ });
1051
+ $type("where: treat date as literal value (not operator)", async (assert) => {
1052
+ const d = new Date();
1053
+ const inserted = await Type.insert({ date: d });
1054
+ // to be treated as equality, not operator parsing.
1055
+ const got = await Type.find({ where: { date: new Date(d.getTime()) } });
1056
+ assert(got.length).equals(1);
1057
+ assert(got[0].id).equals(inserted.id);
1058
+ });
1059
+ $type("where: number operators ($gt/$gte/$lt/$lte/$ne)", async (assert) => {
1060
+ // use a marker to avoid cross-test interference if the DB isn't reset
1061
+ await Type.insert({ string: "ops-num", u8: 201 });
1062
+ await Type.insert({ string: "ops-num", u8: 202 });
1063
+ await Type.insert({ string: "ops-num", u8: 203 });
1064
+ const gt = await Type.find({
1065
+ where: { string: "ops-num", u8: { $gt: 201 } },
1066
+ sort: { u8: "asc" },
1067
+ });
1068
+ assert(gt.map(r => r.u8)).equals([202, 203]);
1069
+ const between = await Type.find({
1070
+ where: { string: "ops-num", u8: { $gte: 202, $lt: 203 } },
1071
+ });
1072
+ assert(between.length).equals(1);
1073
+ assert(between[0].u8).equals(202);
1074
+ const ne = await Type.find({
1075
+ where: { string: "ops-num", u8: { $ne: 202 } },
1076
+ sort: { u8: "asc" },
1077
+ });
1078
+ assert(ne.map(r => r.u8)).equals([201, 203]);
1079
+ const lte = await Type.find({
1080
+ where: { string: "ops-num", u8: { $lte: 202 } },
1081
+ sort: { u8: "asc" },
1082
+ });
1083
+ assert(lte.map(r => r.u8)).equals([201, 202]);
1084
+ });
1085
+ $type("where: bigint operators ($gt/$gte/$lt/$lte/$ne)", async (assert) => {
1086
+ await Type.insert({ string: "ops-big", u64: 201n });
1087
+ await Type.insert({ string: "ops-big", u64: 202n });
1088
+ await Type.insert({ string: "ops-big", u64: 203n });
1089
+ const gt = await Type.find({
1090
+ where: { string: "ops-big", u64: { $gt: 201n } },
1091
+ sort: { u64: "asc" },
1092
+ });
1093
+ assert(gt.map(r => r.u64)).equals([202n, 203n]);
1094
+ const between = await Type.find({
1095
+ where: { string: "ops-big", u64: { $gte: 202n, $lt: 203n } },
1096
+ });
1097
+ assert(between.length).equals(1);
1098
+ assert(between[0].u64).equals(202n);
1099
+ const ne = await Type.find({
1100
+ where: { string: "ops-big", u64: { $ne: 202n } },
1101
+ sort: { u64: "asc" },
1102
+ });
1103
+ assert(ne.map(r => r.u64)).equals([201n, 203n]);
1104
+ });
1105
+ $type("where: datetime operators ($before/$after/$ne)", async (assert) => {
1106
+ const d1 = new Date("2020-01-01T00:00:00.000Z");
1107
+ const d2 = new Date("2020-01-02T00:00:00.000Z");
1108
+ const d3 = new Date("2020-01-03T00:00:00.000Z");
1109
+ await Type.insert({ string: "ops-date", date: d1 });
1110
+ await Type.insert({ string: "ops-date", date: d2 });
1111
+ await Type.insert({ string: "ops-date", date: d3 });
1112
+ const before = await Type.find({
1113
+ where: { string: "ops-date", date: { $before: d2 } },
1114
+ sort: { date: "asc" },
1115
+ });
1116
+ assert(before.map(r => r.date.getTime())).equals([d1.getTime()]);
1117
+ const after = await Type.find({
1118
+ where: { string: "ops-date", date: { $after: d2 } },
1119
+ sort: { date: "asc" },
1120
+ });
1121
+ assert(after.map(r => r.date.getTime())).equals([d3.getTime()]);
1122
+ const ne = await Type.find({
1123
+ where: { string: "ops-date", date: { $ne: d2 } },
1124
+ sort: { date: "asc" },
1125
+ });
1126
+ assert(ne.map(r => r.date.getTime()))
1127
+ .equals([d1.getTime(), d3.getTime()]);
1128
+ });
1129
+ $type("where: operator validation errors", async (assert) => {
1130
+ await throws(assert, Code.operator_empty, () => {
1131
+ return Type.find({ where: { u8: {} } });
1132
+ });
1133
+ // string/time: only $like
1134
+ await throws(assert, Code.operator_unknown, () => {
1135
+ return Type.find({ where: { string: { $gt: 1 } } });
1136
+ });
1137
+ // number: no $like
1138
+ await throws(assert, Code.operator_unknown, () => {
1139
+ return Type.find({ where: { u8: { $like: "x" } } });
1140
+ });
1141
+ // datetime: no $gt
1142
+ await throws(assert, Code.operator_unknown, () => {
1143
+ return Type.find({ where: { date: { $gt: new Date() } } });
1144
+ });
1145
+ await throws(assert, Code.wrong_type, () => {
1146
+ return Type.find({ where: { u8: { $gt: "nope" } } });
1147
+ });
1148
+ await throws(assert, Code.wrong_type, () => {
1149
+ return Type.find({ where: { date: { $before: "nope" } } });
1150
+ });
1151
+ });
1152
+ $type("where: combined operators on same field", async (assert) => {
1153
+ await Type.insert({ string: "combo", u8: 100 });
1154
+ await Type.insert({ string: "combo", u8: 150 });
1155
+ await Type.insert({ string: "combo", u8: 200 });
1156
+ const results = await Type.find({
1157
+ where: {
1158
+ string: "combo",
1159
+ u8: { $gte: 150, $ne: 200 },
1160
+ },
1161
+ sort: { u8: "asc" },
1162
+ });
1163
+ assert(results.map(r => r.u8)).equals([150]);
1164
+ });
1165
+ $type("json: opaque roundtrip", async (assert) => {
1166
+ const json = { a: 1, b: [true, "x"], c: null };
1167
+ const t = await Type.insert({ json });
1168
+ assert(t.json).equals(json);
1169
+ assert((await Type.get(t.id)).json).equals(json);
1170
+ });
1171
+ $type("json: update", async (assert) => {
1172
+ const t = await Type.insert({ json: { x: 1 } });
1173
+ await Type.update(t.id, { set: { json: { x: 2, y: "hello" } } });
1174
+ assert((await Type.get(t.id)).json).equals({ x: 2, y: "hello" });
1175
+ });
1176
+ $type("json: nested object", async (assert) => {
1177
+ const value = { outer: { inner: { deep: true } } };
1178
+ const t = await Type.insert({ json: value });
1179
+ assert((await Type.get(t.id)).json).equals(value);
1180
+ });
1181
+ $type("json: array at root", async (assert) => {
1182
+ const value = [1, "two", false, null];
1183
+ const t = await Type.insert({ json: value });
1184
+ assert((await Type.get(t.id)).json).equals(value);
1185
+ });
1186
+ $type("json: null value unsets field", async (assert) => {
1187
+ const t = await Type.insert({ json: { x: 1 } });
1188
+ await Type.update(t.id, { set: { json: null } });
1189
+ assert((await Type.get(t.id)).json).undefined();
1190
+ });
1191
+ $type("json: empty object", async (assert) => {
1192
+ const t = await Type.insert({ json: {} });
1193
+ assert((await Type.get(t.id)).json).equals({});
1194
+ });
1195
+ $type("json: empty array", async (assert) => {
1196
+ const t = await Type.insert({ json: [] });
1197
+ assert((await Type.get(t.id)).json).equals([]);
1198
+ });
1199
+ $type("json: primitive string", async (assert) => {
1200
+ const t = await Type.insert({ json: "hello" });
1201
+ assert((await Type.get(t.id)).json).equals("hello");
1202
+ });
1203
+ $type("json: primitive number", async (assert) => {
1204
+ const t = await Type.insert({ json: 42 });
1205
+ assert((await Type.get(t.id)).json).equals(42);
1206
+ });
1207
+ $type("json: primitive float", async (assert) => {
1208
+ const t = await Type.insert({ json: 3.14 });
1209
+ assert((await Type.get(t.id)).json).equals(3.14);
1210
+ });
1211
+ $type("json: primitive boolean true", async (assert) => {
1212
+ const t = await Type.insert({ json: true });
1213
+ assert((await Type.get(t.id)).json).equals(true);
1214
+ });
1215
+ $type("json: primitive boolean false", async (assert) => {
1216
+ const t = await Type.insert({ json: false });
1217
+ assert((await Type.get(t.id)).json).equals(false);
1218
+ });
1219
+ $type("json: null inside object survives roundtrip", async (assert) => {
1220
+ const value = { a: 1, b: null };
1221
+ const t = await Type.insert({ json: value });
1222
+ assert((await Type.get(t.id)).json).equals({ a: 1, b: null });
1223
+ });
1224
+ $type("json: null inside array survives roundtrip", async (assert) => {
1225
+ const value = [1, null, "three"];
1226
+ const t = await Type.insert({ json: value });
1227
+ assert((await Type.get(t.id)).json).equals([1, null, "three"]);
1228
+ });
1229
+ $store("where: null matches omitted optional field", User, async (assert) => {
1230
+ const u = await User.insert({ name: "NoLast" });
1231
+ const rows = await User.find({
1232
+ where: { id: u.id, lastname: null },
1233
+ select: ["id"],
1234
+ });
1235
+ assert(rows.length).equals(1);
1236
+ assert(rows[0].id).equals(u.id);
1237
+ });
1238
+ /*$store("json column: inferred type on insert", AuthorWithJSON, async assert => {
1239
+ const author = await AuthorWithJSON.insert({
1240
+ name: "John",
1241
+ articles: [{ title: "First", published: new Date() }],
1242
+ meta: { views: 0, tags: ["typescript"] },
1243
+ });
1244
+
1245
+ assert(author).type<{
1246
+ id: string;
1247
+ name: string;
1248
+ articles?: { title: string; published: Date }[];
1249
+ meta?: { views: number; tags: string[] };
1250
+ notes?: JSONValue;
1251
+ }>();
1252
+ });
1253
+
1254
+ $store("json column: inferred type on find", AuthorWithJSON, async assert => {
1255
+ const authors = await AuthorWithJSON.find();
1256
+ assert(authors).type<{
1257
+ id: string;
1258
+ name: string;
1259
+ articles?: { title: string; published: Date }[];
1260
+ meta?: { views: number; tags: string[] };
1261
+ notes?: JSONValue;
1262
+ }[]>();
1263
+ });
1264
+
1265
+ $store("json column: inferred type on get", AuthorWithJSON, async assert => {
1266
+ const a = await AuthorWithJSON.insert({
1267
+ name: "John",
1268
+ articles: [{ title: "First", published: new Date() }],
1269
+ meta: { views: 0, tags: ["typescript"] },
1270
+ });
1271
+ const author = await AuthorWithJSON.get(a.id);
1272
+
1273
+ assert(author.articles).type<{ title: string; published: Date }[] | undefined>();
1274
+ assert(author.meta).type<{ views: number; tags: string[] } | undefined>();
1275
+ assert(author.notes).type<JSONValue | undefined>();
1276
+
1277
+ // inner field access fully typed
1278
+ assert(author.articles?.[0].title).type<string | undefined>();
1279
+ assert(author.articles?.[0].published).type<Date | undefined>();
1280
+ });
1281
+
1282
+ $store("json column: roundtrip preserves types", AuthorWithJSON, async assert => {
1283
+ const now = new Date();
1284
+ const a = await AuthorWithJSON.insert({
1285
+ name: "John",
1286
+ articles: [{ title: "First", published: now }],
1287
+ meta: { views: 42, tags: ["ts", "primate"] },
1288
+ });
1289
+
1290
+ const author = await AuthorWithJSON.get(a.id);
1291
+
1292
+ assert(author.articles?.[0].title).equals("First");
1293
+ assert(author.articles?.[0].published?.getTime()).equals(now.getTime());
1294
+ assert(author.meta?.views).equals(42);
1295
+ assert(author.meta?.tags).equals(["ts", "primate"]);
1296
+ });
1297
+
1298
+ $store("json column: nested where is typed", AuthorWithJSON, async assert => {
1299
+ // these should compile
1300
+ await AuthorWithJSON.find({ where: { articles: { title: "First" } } });
1301
+ await AuthorWithJSON.find({ where: { meta: { views: { $gt: 10 } } } });
1302
+ await AuthorWithJSON.find({ where: { meta: { tags: { $like: "%ts%" } } } });
1303
+
1304
+ // $contains always available, untyped
1305
+ await AuthorWithJSON.find({ where: { articles: { $contains: { title: "First" } } } });
1306
+ });*/
1307
+ $user("where: null matches unset via update", async (assert) => {
1308
+ const [paul] = await User.find({ where: { name: "Paul" } });
1309
+ await User.update(paul.id, { set: { lastname: null } });
1310
+ const rows = await User.find({
1311
+ where: { id: paul.id, lastname: null },
1312
+ select: ["id"],
1313
+ });
1314
+ assert(rows.length).equals(1);
1315
+ assert(rows[0].id).equals(paul.id);
1316
+ });
1317
+ $user("find: $like: underscore is single-character *", async (assert) => {
1318
+ await User.insert({ name: "A1" });
1319
+ await User.insert({ name: "A12" });
1320
+ const one = await User.find({
1321
+ where: { name: { $like: "A_" } },
1322
+ select: ["name"],
1323
+ sort: { name: "asc" },
1324
+ });
1325
+ assert(one.map(r => r.name)).equals(["A1"]);
1326
+ const two = await User.find({
1327
+ where: { name: { $like: "A__" } },
1328
+ select: ["name"],
1329
+ sort: { name: "asc" },
1330
+ });
1331
+ assert(two.map(r => r.name)).equals(["A12"]);
1332
+ });
1333
+ $user("find: $like: question mark is literal", async (assert) => {
1334
+ await User.insert({ name: "A?" });
1335
+ await User.insert({ name: "A1" });
1336
+ const got = await User.find({
1337
+ where: { name: { $like: "A?" } },
1338
+ select: ["name"],
1339
+ sort: { name: "asc" },
1340
+ });
1341
+ // '?' means only one character
1342
+ assert(got.map(r => r.name)).equals(["A?"]);
1343
+ });
1344
+ $rel("try: does not swallow invalid relation name", async (assert) => {
1345
+ const [row] = await Article.find({ select: ["id"] });
1346
+ await throws(assert, Code.relation_unknown, () => {
1347
+ // programmer error: should throw, not return undefined
1348
+ return Article.try(row.id, { with: { nope: true } });
1349
+ });
1350
+ });
1351
+ $rel$("with: reject non-array relation select", async (assert) => {
1352
+ await throws(assert, Code.select_invalid, () => {
1353
+ return Author.find({
1354
+ with: {
1355
+ articles: {
1356
+ select: {}, // must be array
1357
+ },
1358
+ },
1359
+ });
1360
+ });
1361
+ });
1362
+ $rel$("with: reject non-uint relation limit", async (assert) => {
1363
+ await throws(assert, Code.limit_invalid, () => {
1364
+ return Author.find({
1365
+ with: {
1366
+ articles: {
1367
+ limit: -1, // must be uint
1368
+ },
1369
+ },
1370
+ });
1371
+ });
1372
+ });
1373
+ $rel("one relation returns null when FK missing", async (assert) => {
1374
+ const author_id = "4d0996db-bda9-4f95-ad7c-7075b10d4ba6";
1375
+ const orphan = await Article.insert({
1376
+ title: "Orphan",
1377
+ author_id,
1378
+ });
1379
+ const got = await Article.get(orphan.id, { with: { author: true } });
1380
+ assert(got.author).null();
1381
+ });
1382
+ $user("get/try: happy path", async (assert) => {
1383
+ const [u] = await User.find({ where: { name: "Donald" } });
1384
+ const got = await User.get(u.id);
1385
+ assert(got.id).equals(u.id);
1386
+ const tried = await User.try(u.id);
1387
+ assert(tried?.id).equals(u.id);
1388
+ });
1389
+ $user$("find: unknown option keys throw", async (assert) => {
1390
+ await throws(assert, Code.option_unknown, () => {
1391
+ // wrong call-shape (looks like where but isn't nested)
1392
+ return User.find({ name: "John" });
1393
+ });
1394
+ await throws(assert, Code.option_unknown, () => {
1395
+ // totally unknown option
1396
+ return User.find({ banana: true });
1397
+ });
1398
+ });
1399
+ $user("find: $like is case-sensitive", async (assert) => {
1400
+ await User.insert({ name: "MiXeDCase" });
1401
+ const no = await User.find({ where: { name: { $like: "mixed%" } } });
1402
+ assert(no.length).equals(0);
1403
+ const yes = await User.find({ where: { name: { $like: "MiXeD%" } } });
1404
+ assert(yes.length).equals(1);
1405
+ assert(yes[0].name).equals("MiXeDCase");
1406
+ });
1407
+ $user("find: $ilike is case-insensitive", async (assert) => {
1408
+ await User.insert({ name: "MiXeDCase2" });
1409
+ const rows = await User.find({ where: { name: { $ilike: "mixed%" } } });
1410
+ assert(rows.length).equals(1);
1411
+ assert(rows[0].name).equals("MiXeDCase2");
1412
+ });
1413
+ $user$("get: unknown option keys throw", async (assert) => {
1414
+ const u = await User.insert({ name: "Guard", age: 1 });
1415
+ await throws(assert, Code.option_unknown, () => {
1416
+ // get only accepts { select?, with? }
1417
+ return User.get(u.id, { where: { name: "Guard" } });
1418
+ });
1419
+ await throws(assert, Code.option_unknown, () => {
1420
+ return User.get(u.id, { sort: { name: "asc" } });
1421
+ });
1422
+ await throws(assert, Code.option_unknown, () => {
1423
+ return User.get(u.id, { banana: true });
1424
+ });
1425
+ });
1426
+ $user$("inject invalid identifier (table name)", async (assert) => {
1427
+ await throws(assert, Code.identifier_invalid, () => {
1428
+ store({
1429
+ name: "users; DROP TABLE users",
1430
+ db,
1431
+ schema: {
1432
+ id: key.primary(p.uuid),
1433
+ },
1434
+ });
1435
+ });
1436
+ });
1437
+ $user("find: $like: literal percent sign", async (assert) => {
1438
+ await User.insert({ name: "100% complete" });
1439
+ await User.insert({ name: "100 complete" });
1440
+ const got = await User.find({
1441
+ where: { name: { $like: "100\\% complete" } },
1442
+ select: ["name"],
1443
+ });
1444
+ assert(got.length).equals(1);
1445
+ assert(got[0].name).equals("100% complete");
1446
+ });
1447
+ $user("find: $like: literal underscore", async (assert) => {
1448
+ await User.insert({ name: "file_name" });
1449
+ await User.insert({ name: "file1name" });
1450
+ const got = await User.find({
1451
+ where: { name: { $like: "file\\_name" } },
1452
+ select: ["name"],
1453
+ });
1454
+ assert(got.length).equals(1);
1455
+ assert(got[0].name).equals("file_name");
1456
+ });
1457
+ $rel("with + select (no id)", async (assert) => {
1458
+ const rows = await Author.find({
1459
+ select: ["name"],
1460
+ with: { articles: { select: ["title"], sort: { title: "asc" } } },
1461
+ sort: { name: "asc" },
1462
+ });
1463
+ // no id leaked
1464
+ for (const r of rows)
1465
+ assert("id" in r).false();
1466
+ // but relations are loaded
1467
+ const john = rows.find(r => r.name === "John");
1468
+ assert(Array.isArray(john.articles)).true();
1469
+ assert(john.articles.length).nequals(0);
1470
+ });
1471
+ $rel("with: relation fields are decoded (URL)", async (assert) => {
1472
+ const [first] = await Author.find({ where: { name: "John" } });
1473
+ const john = await Author.get(first.id, {
1474
+ with: { profile: true },
1475
+ });
1476
+ assert(john.profile).not.null();
1477
+ // this is the whole point: driver must unbind it
1478
+ assert(john.profile.url).type();
1479
+ assert(john.profile.url instanceof URL).true();
1480
+ assert(john.profile.url?.href).equals("https://example.com/john");
1481
+ });
1482
+ $rel("reverse one: with + select (no author_id) still loads relation", async (assert) => {
1483
+ const [row] = await Article.find({ select: ["id"] });
1484
+ const got = await Article.get(row.id, {
1485
+ select: ["title"], // intentionally omit author_id
1486
+ with: { author: true },
1487
+ });
1488
+ assert("author_id" in got).false(); // stripped
1489
+ assert(got.author).not.null();
1490
+ });
1491
+ $rel("with: joined relations decode URL fields (base + rel)", async (assert) => {
1492
+ const ParentSchema = {
1493
+ id: key.primary(p.uuid),
1494
+ name: p.string,
1495
+ url: p.url.optional(),
1496
+ };
1497
+ const ChildSchema = {
1498
+ id: key.primary(p.uuid),
1499
+ parent_id: key.foreign(p.uuid),
1500
+ url: p.url.optional(),
1501
+ };
1502
+ const Parent = store({
1503
+ name: "j_parent",
1504
+ db,
1505
+ schema: ParentSchema,
1506
+ relations: {
1507
+ children: relation.many(ChildSchema, "parent_id"),
1508
+ },
1509
+ });
1510
+ const Child = store({
1511
+ db,
1512
+ name: "j_child",
1513
+ schema: ChildSchema,
1514
+ relations: {
1515
+ parent: relation.one(ParentSchema, "parent_id", { reverse: true }),
1516
+ },
1517
+ });
1518
+ await Parent.table.create();
1519
+ await Child.table.create();
1520
+ try {
1521
+ const p0 = await Parent.insert({
1522
+ name: "P0",
1523
+ url: new URL("https://example.com/parent"),
1524
+ });
1525
+ await Child.insert({
1526
+ parent_id: p0.id,
1527
+ url: new URL("https://example.com/joined"),
1528
+ });
1529
+ // IMPORTANT: force joined path + projection that omits pk, but still must join.
1530
+ const [got] = await Parent.find({
1531
+ where: { id: p0.id },
1532
+ select: ["url"],
1533
+ with: { children: true },
1534
+ });
1535
+ assert(got).defined();
1536
+ // pk must not leak
1537
+ assert("id" in got).false();
1538
+ // ---- base assertion (should fail if joined base isn't unbound/decoded)
1539
+ assert(got.url).type();
1540
+ assert(got.url instanceof URL).true();
1541
+ assert(got.url.href).equals("https://example.com/parent");
1542
+ // ---- relation assertion (should fail if joined relation isn't unbound/decoded)
1543
+ assert(Array.isArray(got.children)).true();
1544
+ assert(got.children.length).equals(1);
1545
+ const c0 = got.children[0];
1546
+ assert(c0.url).type();
1547
+ assert(c0.url instanceof URL).true();
1548
+ assert(c0.url.href).equals("https://example.com/joined");
1549
+ }
1550
+ finally {
1551
+ await Child.table.delete();
1552
+ await Parent.table.delete();
1553
+ }
1554
+ });
1555
+ $rel("find: limit applies to parent rows, not joined rows", async (assert) => {
1556
+ const authors = await Author.find({
1557
+ limit: 2,
1558
+ sort: { name: "asc" },
1559
+ with: { articles: true },
1560
+ });
1561
+ assert(authors.length).equals(2);
1562
+ });
1563
+ for (const c of BAD_WHERE) {
1564
+ bad_where(c.label, () => ({ base: c.base, with: c.with }), c.expected);
1565
+ }
1566
+ for (const c of BAD_SELECT) {
1567
+ bad_select(c.label, () => ({ base: c.base, with: c.with }), c.expected);
1568
+ }
1569
+ for (const c of BAD_SORT) {
1570
+ bad_sort(c.label, () => ({ base: c.base, with: c.with }), c.expected);
1571
+ }
1572
+ for (const c of BAD_WHERE_COLUMN) {
1573
+ bad_where(c.label, () => ({ base: c.base, with: c.with }), c.expected);
1574
+ }
1575
+ test.case("store: relation name conflicts with field throws", async (assert) => {
1576
+ await throws(assert, Code.relation_conflicts_with_field, async () => store({
1577
+ name: "conflict",
1578
+ db,
1579
+ schema: {
1580
+ id: key.primary(p.uuid),
1581
+ articles: p.string,
1582
+ },
1583
+ relations: {
1584
+ articles: relation.many(ArticleSchema, "author_id"),
1585
+ },
1586
+ }));
1587
+ });
1588
+ // introspect: returns null for non-existent table
1589
+ test.case("schema: introspect non-existent", async (assert) => {
1590
+ assert(await db.schema.introspect("no_such_table")).null();
1591
+ });
1592
+ // UUID pk (User): wrong JS type
1593
+ $store("pk: get rejects non-string for uuid pk", User, async (assert) => {
1594
+ await throws(assert, Code.pk_invalid, () => User.get(123));
1595
+ });
1596
+ $store("pk: has rejects non-string for uuid pk", User, async (assert) => {
1597
+ await throws(assert, Code.pk_invalid, () => User.has(123));
1598
+ });
1599
+ $store("pk: delete rejects non-string for uuid pk", User, async (assert) => {
1600
+ await throws(assert, Code.pk_invalid, () => User.delete(123));
1601
+ });
1602
+ $store("pk: update rejects non-string for uuid pk", User, async (assert) => {
1603
+ await throws(assert, Code.pk_invalid, () => User.update(123, { set: { name: "x" } }));
1604
+ });
1605
+ // UUID pk (User): valid JS type but malformed UUID
1606
+ $store("pk: get rejects malformed uuid", User, async (assert) => {
1607
+ await throws(assert, Code.pk_invalid, () => User.get("not-a-uuid"));
1608
+ });
1609
+ $store("pk: has rejects malformed uuid", User, async (assert) => {
1610
+ await throws(assert, Code.pk_invalid, () => User.has("not-a-uuid"));
1611
+ });
1612
+ $store("pk: delete rejects malformed uuid", User, async (assert) => {
1613
+ await throws(assert, Code.pk_invalid, () => User.delete("not-a-uuid"));
1614
+ });
1615
+ $store("pk: update rejects malformed uuid", User, async (assert) => {
1616
+ await throws(assert, Code.pk_invalid, () => User.update("not-a-uuid", { set: { name: "x" } }));
1617
+ });
1618
+ // Numeric pk (UserN u32): wrong JS type
1619
+ $store("pk: get rejects string for numeric pk", UserN, async (assert) => {
1620
+ await throws(assert, Code.pk_invalid, () => UserN.get("abc"));
1621
+ });
1622
+ $store("pk: has rejects string for numeric pk", UserN, async (assert) => {
1623
+ await throws(assert, Code.pk_invalid, () => UserN.has("abc"));
1624
+ });
1625
+ $store("pk: delete rejects string for numeric pk", UserN, async (assert) => {
1626
+ await throws(assert, Code.pk_invalid, () => UserN.delete("abc"));
1627
+ });
1628
+ $store("pk: update rejects string for numeric pk", UserN, async (assert) => {
1629
+ await throws(assert, Code.pk_invalid, () => UserN.update("abc", { set: { name: "x" } }));
1630
+ });
1631
+ // Numeric pk (UserN u32): negative value
1632
+ $store("pk: get rejects negative for unsigned pk", UserN, async (assert) => {
1633
+ await throws(assert, Code.pk_invalid, () => UserN.get(-1));
1634
+ });
1635
+ // Bigint pk (UserB u128): wrong JS type
1636
+ $store("pk: get rejects string for bigint pk", UserB, async (assert) => {
1637
+ await throws(assert, Code.pk_invalid, () => UserB.get("abc"));
1638
+ });
1639
+ $store("pk: get rejects negative for unsigned bigint pk", UserB, async (assert) => {
1640
+ await throws(assert, Code.pk_invalid, () => UserB.get(-1n));
1641
+ });
1642
+ // introspect: returns correct types after create
1643
+ $store("schema: introspect after create", User, async (assert) => {
1644
+ // insert a record so MongoDB has something to sample
1645
+ await User.insert({ name: "test", age: 1, lastname: "test" });
1646
+ const types = await db.schema.introspect(User.name, User.pk);
1647
+ assert(types).not.null();
1648
+ if (types !== null) {
1649
+ assert(types.id).includes("uuid");
1650
+ assert(types.name).includes("string");
1651
+ assert(types.age).includes("u8");
1652
+ assert(types.lastname).includes("string");
1653
+ }
1654
+ });
1655
+ // alter: add a field
1656
+ $store("schema: alter add", User, async (assert) => {
1657
+ await db.schema.alter("user", {
1658
+ add: { email: "string" }, drop: [], rename: [],
1659
+ });
1660
+ const MigratedUser = store({
1661
+ name: "user",
1662
+ db,
1663
+ schema: {
1664
+ id: key.primary(p.uuid),
1665
+ age: p.u8.optional(),
1666
+ lastname: p.string.optional(),
1667
+ name: p.string.default("Donald"),
1668
+ email: p.string.email(),
1669
+ },
1670
+ });
1671
+ await MigratedUser.insert({ name: "test", age: 1, email: "test@test.com" });
1672
+ const types = await db.schema.introspect("user");
1673
+ assert(types.email).includes("string");
1674
+ });
1675
+ // alter: drop a field
1676
+ $store("schema: alter drop", User, async (assert) => {
1677
+ await db.schema.alter("user", { add: {}, drop: ["lastname"], rename: [] });
1678
+ const types = await db.schema.introspect("user");
1679
+ assert(types.lastname).undefined();
1680
+ });
1681
+ // alter: rename a field
1682
+ $store("schema: alter rename", User, async (assert) => {
1683
+ await db.schema.alter("user", {
1684
+ add: {}, drop: [], rename: [["lastname", "surname"]],
1685
+ });
1686
+ const MigratedUser = store({
1687
+ name: "user",
1688
+ db,
1689
+ schema: {
1690
+ id: key.primary(p.uuid),
1691
+ age: p.u8.optional(),
1692
+ surname: p.string.optional(),
1693
+ name: p.string.default("Donald"),
1694
+ },
1695
+ });
1696
+ await MigratedUser.insert({ name: "Donald", surname: "Adams" });
1697
+ const types = await db.schema.introspect("user");
1698
+ assert(types.lastname).undefined();
1699
+ assert(types.surname).includes("string");
1700
+ });
1701
+ // alter: combined add + drop
1702
+ $store("schema: alter add and drop", User, async (assert) => {
1703
+ await db.schema.alter("user", {
1704
+ add: { email: "string" }, drop: ["lastname"], rename: [],
1705
+ });
1706
+ const MigratedUser = store({
1707
+ name: "user",
1708
+ db,
1709
+ schema: {
1710
+ id: key.primary(p.uuid),
1711
+ age: p.u8.optional(),
1712
+ name: p.string.default("Donald"),
1713
+ email: p.string.email(),
1714
+ },
1715
+ });
1716
+ await MigratedUser.insert({ name: "Donald", email: "test@test.com" });
1717
+ const types = await db.schema.introspect("user");
1718
+ assert(types.email).includes("string");
1719
+ assert(types.lastname).undefined();
1720
+ });
1721
+ const UUIDTest = store({
1722
+ name: "uuid_test",
1723
+ db,
1724
+ schema: {
1725
+ id: key.primary(p.uuid),
1726
+ uuid: p.uuid,
1727
+ uuid_v4: p.uuid.v4(),
1728
+ uuid_v7: p.uuid.v7(),
1729
+ },
1730
+ });
1731
+ $store("uuid: round-trips uuid column", UUIDTest, async (assert) => {
1732
+ const uuid = "4d0996db-bda9-4f95-ad7c-7075b10d4ba6"; // any valid uuid
1733
+ const uuid_v4 = "7c9e6679-7425-40de-944b-e07fc1f90ae7"; // distinct v4
1734
+ const uuid_v7 = "01932b6e-5a2f-7e4f-9a3b-4f6d2c8b1a0e"; // v7
1735
+ const row = await UUIDTest.insert({ uuid, uuid_v4, uuid_v7 });
1736
+ assert(row.uuid).equals(uuid).type();
1737
+ assert(row.uuid_v4).equals(uuid_v4).type();
1738
+ assert(row.uuid_v7).equals(uuid_v7).type();
1739
+ const fetched = await UUIDTest.get(row.id);
1740
+ assert(fetched.uuid).equals(uuid);
1741
+ assert(fetched.uuid_v4).equals(uuid_v4);
1742
+ assert(fetched.uuid_v7).equals(uuid_v7);
1743
+ });
1744
+ test.case("schema: alter non-existent throws", async (assert) => {
1745
+ await throws(assert, Code.table_not_found, () => db.schema.alter("no_such_table", {
1746
+ add: { email: "string" }, drop: [], rename: [],
1747
+ }));
1748
+ });
1749
+ };
1750
+ //# sourceMappingURL=test.js.map