@btst/stack 1.0.1 → 1.1.1

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 (282) hide show
  1. package/README.md +156 -709
  2. package/dist/api/index.cjs +2 -1
  3. package/dist/api/index.d.cts +4 -3
  4. package/dist/api/index.d.mts +4 -3
  5. package/dist/api/index.d.ts +4 -3
  6. package/dist/api/index.mjs +1 -1
  7. package/dist/client/components/compose.cjs +68 -0
  8. package/dist/client/components/compose.mjs +65 -0
  9. package/dist/client/components/error-boundary.cjs +24 -0
  10. package/dist/client/components/error-boundary.mjs +22 -0
  11. package/dist/client/components/index.cjs +10 -0
  12. package/dist/client/components/index.d.cts +52 -0
  13. package/dist/client/components/index.d.mts +52 -0
  14. package/dist/client/components/index.d.ts +52 -0
  15. package/dist/client/components/index.mjs +2 -0
  16. package/dist/client/index.cjs +24 -5
  17. package/dist/client/index.d.cts +125 -8
  18. package/dist/client/index.d.mts +125 -8
  19. package/dist/client/index.d.ts +125 -8
  20. package/dist/client/index.mjs +21 -4
  21. package/dist/client/meta-utils.cjs +162 -0
  22. package/dist/client/meta-utils.mjs +160 -0
  23. package/dist/client/path-utils.cjs +15 -0
  24. package/dist/client/path-utils.mjs +13 -0
  25. package/dist/client/sitemap-utils.cjs +14 -0
  26. package/dist/client/sitemap-utils.mjs +12 -0
  27. package/dist/context/index.cjs +6 -63
  28. package/dist/context/index.d.cts +21 -24
  29. package/dist/context/index.d.mts +21 -24
  30. package/dist/context/index.d.ts +21 -24
  31. package/dist/context/index.mjs +1 -61
  32. package/dist/context/provider.cjs +51 -0
  33. package/dist/context/provider.mjs +46 -0
  34. package/dist/index.cjs +2 -3
  35. package/dist/index.d.cts +3 -2
  36. package/dist/index.d.mts +3 -2
  37. package/dist/index.d.ts +3 -2
  38. package/dist/index.mjs +1 -2
  39. package/dist/plugins/api/index.cjs +13 -0
  40. package/dist/plugins/api/index.d.cts +40 -0
  41. package/dist/plugins/api/index.d.mts +40 -0
  42. package/dist/plugins/api/index.d.ts +40 -0
  43. package/dist/plugins/api/index.mjs +8 -0
  44. package/dist/plugins/blog/api/index.cjs +11 -0
  45. package/dist/plugins/blog/api/index.d.cts +7 -0
  46. package/dist/plugins/blog/api/index.d.mts +7 -0
  47. package/dist/plugins/blog/api/index.d.ts +7 -0
  48. package/dist/plugins/blog/api/index.mjs +2 -0
  49. package/dist/plugins/blog/api/plugin.cjs +569 -0
  50. package/dist/plugins/blog/api/plugin.mjs +565 -0
  51. package/dist/plugins/blog/client/components/forms/image-field.cjs +133 -0
  52. package/dist/plugins/blog/client/components/forms/image-field.mjs +131 -0
  53. package/dist/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
  54. package/dist/plugins/blog/client/components/forms/markdown-editor.cjs +106 -0
  55. package/dist/plugins/blog/client/components/forms/markdown-editor.mjs +104 -0
  56. package/dist/plugins/blog/client/components/forms/post-forms.cjs +401 -0
  57. package/dist/plugins/blog/client/components/forms/post-forms.mjs +398 -0
  58. package/dist/plugins/blog/client/components/forms/tags-multiselect.cjs +71 -0
  59. package/dist/plugins/blog/client/components/forms/tags-multiselect.mjs +65 -0
  60. package/dist/plugins/blog/client/components/index.cjs +17 -0
  61. package/dist/plugins/blog/client/components/index.d.cts +22 -0
  62. package/dist/plugins/blog/client/components/index.d.mts +22 -0
  63. package/dist/plugins/blog/client/components/index.d.ts +22 -0
  64. package/dist/plugins/blog/client/components/index.mjs +12 -0
  65. package/dist/plugins/blog/client/components/loading/form-page-skeleton.cjs +62 -0
  66. package/dist/plugins/blog/client/components/loading/form-page-skeleton.mjs +60 -0
  67. package/dist/plugins/blog/client/components/loading/index.cjs +20 -0
  68. package/dist/plugins/blog/client/components/loading/index.mjs +16 -0
  69. package/dist/plugins/blog/client/components/loading/list-page-skeleton.cjs +26 -0
  70. package/dist/plugins/blog/client/components/loading/list-page-skeleton.mjs +24 -0
  71. package/dist/plugins/blog/client/components/loading/page-header-skeleton.cjs +13 -0
  72. package/dist/plugins/blog/client/components/loading/page-header-skeleton.mjs +11 -0
  73. package/dist/plugins/blog/client/components/loading/post-card-skeleton.cjs +22 -0
  74. package/dist/plugins/blog/client/components/loading/post-card-skeleton.mjs +20 -0
  75. package/dist/plugins/blog/client/components/loading/post-page-skeleton.cjs +56 -0
  76. package/dist/plugins/blog/client/components/loading/post-page-skeleton.mjs +54 -0
  77. package/dist/plugins/blog/client/components/pages/404-page.cjs +19 -0
  78. package/dist/plugins/blog/client/components/pages/404-page.mjs +17 -0
  79. package/dist/plugins/blog/client/components/pages/edit-post-page.cjs +41 -0
  80. package/dist/plugins/blog/client/components/pages/edit-post-page.internal.cjs +57 -0
  81. package/dist/plugins/blog/client/components/pages/edit-post-page.internal.mjs +55 -0
  82. package/dist/plugins/blog/client/components/pages/edit-post-page.mjs +39 -0
  83. package/dist/plugins/blog/client/components/pages/home-page.cjs +41 -0
  84. package/dist/plugins/blog/client/components/pages/home-page.internal.cjs +61 -0
  85. package/dist/plugins/blog/client/components/pages/home-page.internal.mjs +59 -0
  86. package/dist/plugins/blog/client/components/pages/home-page.mjs +39 -0
  87. package/dist/plugins/blog/client/components/pages/new-post-page.cjs +37 -0
  88. package/dist/plugins/blog/client/components/pages/new-post-page.internal.cjs +53 -0
  89. package/dist/plugins/blog/client/components/pages/new-post-page.internal.mjs +51 -0
  90. package/dist/plugins/blog/client/components/pages/new-post-page.mjs +35 -0
  91. package/dist/plugins/blog/client/components/pages/post-page.cjs +39 -0
  92. package/dist/plugins/blog/client/components/pages/post-page.internal.cjs +101 -0
  93. package/dist/plugins/blog/client/components/pages/post-page.internal.mjs +99 -0
  94. package/dist/plugins/blog/client/components/pages/post-page.mjs +37 -0
  95. package/dist/plugins/blog/client/components/pages/tag-page.cjs +39 -0
  96. package/dist/plugins/blog/client/components/pages/tag-page.internal.cjs +61 -0
  97. package/dist/plugins/blog/client/components/pages/tag-page.internal.mjs +59 -0
  98. package/dist/plugins/blog/client/components/pages/tag-page.mjs +37 -0
  99. package/dist/plugins/blog/client/components/shared/better-blog-attribution.cjs +24 -0
  100. package/dist/plugins/blog/client/components/shared/better-blog-attribution.mjs +22 -0
  101. package/dist/plugins/blog/client/components/shared/default-error.cjs +18 -0
  102. package/dist/plugins/blog/client/components/shared/default-error.mjs +16 -0
  103. package/dist/plugins/blog/client/components/shared/defaults.cjs +13 -0
  104. package/dist/plugins/blog/client/components/shared/defaults.mjs +10 -0
  105. package/dist/plugins/blog/client/components/shared/empty-list.cjs +21 -0
  106. package/dist/plugins/blog/client/components/shared/empty-list.mjs +19 -0
  107. package/dist/plugins/blog/client/components/shared/error-placeholder.cjs +24 -0
  108. package/dist/plugins/blog/client/components/shared/error-placeholder.mjs +22 -0
  109. package/dist/plugins/blog/client/components/shared/highlight-text.cjs +53 -0
  110. package/dist/plugins/blog/client/components/shared/highlight-text.mjs +51 -0
  111. package/dist/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
  112. package/dist/plugins/blog/client/components/shared/markdown-content.cjs +324 -0
  113. package/dist/plugins/blog/client/components/shared/markdown-content.mjs +315 -0
  114. package/dist/plugins/blog/client/components/shared/on-this-page.cjs +161 -0
  115. package/dist/plugins/blog/client/components/shared/on-this-page.mjs +158 -0
  116. package/dist/plugins/blog/client/components/shared/page-header.cjs +40 -0
  117. package/dist/plugins/blog/client/components/shared/page-header.mjs +38 -0
  118. package/dist/plugins/blog/client/components/shared/page-layout.cjs +24 -0
  119. package/dist/plugins/blog/client/components/shared/page-layout.mjs +22 -0
  120. package/dist/plugins/blog/client/components/shared/page-wrapper.cjs +23 -0
  121. package/dist/plugins/blog/client/components/shared/page-wrapper.mjs +21 -0
  122. package/dist/plugins/blog/client/components/shared/post-card.cjs +279 -0
  123. package/dist/plugins/blog/client/components/shared/post-card.mjs +277 -0
  124. package/dist/plugins/blog/client/components/shared/post-navigation.cjs +74 -0
  125. package/dist/plugins/blog/client/components/shared/post-navigation.mjs +72 -0
  126. package/dist/plugins/blog/client/components/shared/posts-list.cjs +48 -0
  127. package/dist/plugins/blog/client/components/shared/posts-list.mjs +46 -0
  128. package/dist/plugins/blog/client/components/shared/recent-posts-carousel.cjs +59 -0
  129. package/dist/plugins/blog/client/components/shared/recent-posts-carousel.mjs +57 -0
  130. package/dist/plugins/blog/client/components/shared/search-input.cjs +136 -0
  131. package/dist/plugins/blog/client/components/shared/search-input.mjs +117 -0
  132. package/dist/plugins/blog/client/components/shared/search-modal.cjs +135 -0
  133. package/dist/plugins/blog/client/components/shared/search-modal.mjs +116 -0
  134. package/dist/plugins/blog/client/components/shared/tags-list.cjs +22 -0
  135. package/dist/plugins/blog/client/components/shared/tags-list.mjs +20 -0
  136. package/dist/plugins/blog/client/components/shared/use-route-lifecycle.cjs +50 -0
  137. package/dist/plugins/blog/client/components/shared/use-route-lifecycle.mjs +48 -0
  138. package/dist/plugins/blog/client/hooks/blog-hooks.cjs +380 -0
  139. package/dist/plugins/blog/client/hooks/blog-hooks.mjs +368 -0
  140. package/dist/plugins/blog/client/hooks/index.cjs +17 -0
  141. package/dist/plugins/blog/client/hooks/index.d.cts +150 -0
  142. package/dist/plugins/blog/client/hooks/index.d.mts +150 -0
  143. package/dist/plugins/blog/client/hooks/index.d.ts +150 -0
  144. package/dist/plugins/blog/client/hooks/index.mjs +1 -0
  145. package/dist/plugins/blog/client/hooks/use-debounce.cjs +16 -0
  146. package/dist/plugins/blog/client/hooks/use-debounce.mjs +14 -0
  147. package/dist/plugins/blog/client/index.cjs +7 -0
  148. package/dist/plugins/blog/client/index.d.cts +414 -0
  149. package/dist/plugins/blog/client/index.d.mts +414 -0
  150. package/dist/plugins/blog/client/index.d.ts +414 -0
  151. package/dist/plugins/blog/client/index.mjs +1 -0
  152. package/dist/plugins/blog/client/localization/blog-card.cjs +7 -0
  153. package/dist/plugins/blog/client/localization/blog-card.mjs +5 -0
  154. package/dist/plugins/blog/client/localization/blog-common.cjs +10 -0
  155. package/dist/plugins/blog/client/localization/blog-common.mjs +8 -0
  156. package/dist/plugins/blog/client/localization/blog-forms.cjs +40 -0
  157. package/dist/plugins/blog/client/localization/blog-forms.mjs +38 -0
  158. package/dist/plugins/blog/client/localization/blog-list.cjs +18 -0
  159. package/dist/plugins/blog/client/localization/blog-list.mjs +16 -0
  160. package/dist/plugins/blog/client/localization/blog-post.cjs +13 -0
  161. package/dist/plugins/blog/client/localization/blog-post.mjs +11 -0
  162. package/dist/plugins/blog/client/localization/index.cjs +17 -0
  163. package/dist/plugins/blog/client/localization/index.mjs +15 -0
  164. package/dist/plugins/blog/client/plugin.cjs +462 -0
  165. package/dist/plugins/blog/client/plugin.mjs +460 -0
  166. package/dist/plugins/blog/client.css +3 -0
  167. package/dist/plugins/blog/db.cjs +90 -0
  168. package/dist/plugins/blog/db.mjs +88 -0
  169. package/dist/plugins/blog/query-keys.cjs +181 -0
  170. package/dist/plugins/blog/query-keys.d.cts +530 -0
  171. package/dist/plugins/blog/query-keys.d.mts +530 -0
  172. package/dist/plugins/blog/query-keys.d.ts +530 -0
  173. package/dist/plugins/blog/query-keys.mjs +179 -0
  174. package/dist/plugins/blog/schemas.cjs +39 -0
  175. package/dist/plugins/blog/schemas.mjs +35 -0
  176. package/dist/plugins/blog/style.css +22 -0
  177. package/dist/plugins/blog/utils.cjs +97 -0
  178. package/dist/plugins/blog/utils.mjs +87 -0
  179. package/dist/plugins/client/index.cjs +15 -0
  180. package/dist/plugins/client/index.d.cts +57 -0
  181. package/dist/plugins/client/index.d.mts +57 -0
  182. package/dist/plugins/client/index.d.ts +57 -0
  183. package/dist/plugins/client/index.mjs +9 -0
  184. package/dist/{shared/stack.3OUyGp_E.mjs → plugins/utils.mjs} +1 -1
  185. package/dist/shared/{stack.DORw_1ps.d.cts → stack.ByOugz9d.d.cts} +17 -1
  186. package/dist/shared/{stack.DORw_1ps.d.mts → stack.ByOugz9d.d.mts} +17 -1
  187. package/dist/shared/{stack.DORw_1ps.d.ts → stack.ByOugz9d.d.ts} +17 -1
  188. package/dist/shared/stack.CoPoHVfV.d.cts +76 -0
  189. package/dist/shared/stack.CoPoHVfV.d.mts +76 -0
  190. package/dist/shared/stack.CoPoHVfV.d.ts +76 -0
  191. package/package.json +102 -14
  192. package/src/__tests__/plugins.test.tsx +539 -0
  193. package/src/__tests__/sitemap.test.ts +60 -0
  194. package/src/api/index.ts +75 -0
  195. package/src/client/components/compose.tsx +116 -0
  196. package/src/client/components/error-boundary.tsx +30 -0
  197. package/src/client/components/index.tsx +2 -0
  198. package/src/client/index.ts +109 -0
  199. package/src/client/meta-utils.ts +228 -0
  200. package/src/client/path-utils.ts +38 -0
  201. package/src/client/sitemap-utils.ts +46 -0
  202. package/src/context/index.ts +1 -0
  203. package/src/context/provider.tsx +157 -0
  204. package/src/index.ts +1 -0
  205. package/src/plugins/api/index.ts +50 -0
  206. package/src/plugins/blog/api/index.ts +2 -0
  207. package/src/plugins/blog/api/plugin.ts +759 -0
  208. package/src/plugins/blog/client/components/forms/image-field.tsx +165 -0
  209. package/src/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
  210. package/src/plugins/blog/client/components/forms/markdown-editor.tsx +136 -0
  211. package/src/plugins/blog/client/components/forms/post-forms.tsx +531 -0
  212. package/src/plugins/blog/client/components/forms/tags-multiselect.tsx +79 -0
  213. package/src/plugins/blog/client/components/index.tsx +11 -0
  214. package/src/plugins/blog/client/components/loading/form-page-skeleton.tsx +75 -0
  215. package/src/plugins/blog/client/components/loading/index.tsx +27 -0
  216. package/src/plugins/blog/client/components/loading/list-page-skeleton.tsx +38 -0
  217. package/src/plugins/blog/client/components/loading/page-header-skeleton.tsx +10 -0
  218. package/src/plugins/blog/client/components/loading/post-card-skeleton.tsx +30 -0
  219. package/src/plugins/blog/client/components/loading/post-page-skeleton.tsx +75 -0
  220. package/src/plugins/blog/client/components/pages/404-page.tsx +23 -0
  221. package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +60 -0
  222. package/src/plugins/blog/client/components/pages/edit-post-page.tsx +40 -0
  223. package/src/plugins/blog/client/components/pages/home-page.internal.tsx +71 -0
  224. package/src/plugins/blog/client/components/pages/home-page.tsx +42 -0
  225. package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +59 -0
  226. package/src/plugins/blog/client/components/pages/new-post-page.tsx +36 -0
  227. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +142 -0
  228. package/src/plugins/blog/client/components/pages/post-page.tsx +38 -0
  229. package/src/plugins/blog/client/components/pages/tag-page.internal.tsx +74 -0
  230. package/src/plugins/blog/client/components/pages/tag-page.tsx +38 -0
  231. package/src/plugins/blog/client/components/shared/better-blog-attribution.tsx +19 -0
  232. package/src/plugins/blog/client/components/shared/default-error.tsx +20 -0
  233. package/src/plugins/blog/client/components/shared/defaults.tsx +9 -0
  234. package/src/plugins/blog/client/components/shared/empty-list.tsx +25 -0
  235. package/src/plugins/blog/client/components/shared/error-placeholder.tsx +20 -0
  236. package/src/plugins/blog/client/components/shared/highlight-text.tsx +80 -0
  237. package/src/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
  238. package/src/plugins/blog/client/components/shared/markdown-content.tsx +448 -0
  239. package/src/plugins/blog/client/components/shared/on-this-page.tsx +234 -0
  240. package/src/plugins/blog/client/components/shared/page-header.tsx +35 -0
  241. package/src/plugins/blog/client/components/shared/page-layout.tsx +23 -0
  242. package/src/plugins/blog/client/components/shared/page-wrapper.tsx +32 -0
  243. package/src/plugins/blog/client/components/shared/post-card.tsx +308 -0
  244. package/src/plugins/blog/client/components/shared/post-navigation.tsx +98 -0
  245. package/src/plugins/blog/client/components/shared/posts-list.tsx +67 -0
  246. package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +79 -0
  247. package/src/plugins/blog/client/components/shared/search-input.tsx +146 -0
  248. package/src/plugins/blog/client/components/shared/search-modal.tsx +162 -0
  249. package/src/plugins/blog/client/components/shared/tags-list.tsx +34 -0
  250. package/src/plugins/blog/client/components/shared/use-route-lifecycle.tsx +68 -0
  251. package/src/plugins/blog/client/hooks/blog-hooks.tsx +623 -0
  252. package/src/plugins/blog/client/hooks/index.tsx +1 -0
  253. package/src/plugins/blog/client/hooks/use-debounce.ts +43 -0
  254. package/src/plugins/blog/client/index.ts +9 -0
  255. package/src/plugins/blog/client/localization/blog-card.ts +3 -0
  256. package/src/plugins/blog/client/localization/blog-common.ts +7 -0
  257. package/src/plugins/blog/client/localization/blog-forms.ts +45 -0
  258. package/src/plugins/blog/client/localization/blog-list.ts +14 -0
  259. package/src/plugins/blog/client/localization/blog-post.ts +9 -0
  260. package/src/plugins/blog/client/localization/index.ts +15 -0
  261. package/src/plugins/blog/client/overrides.ts +123 -0
  262. package/src/plugins/blog/client/plugin.tsx +672 -0
  263. package/src/plugins/blog/client.css +3 -0
  264. package/src/plugins/blog/db.ts +90 -0
  265. package/src/plugins/blog/query-keys.ts +267 -0
  266. package/src/plugins/blog/schemas.ts +39 -0
  267. package/src/plugins/blog/style.css +22 -0
  268. package/src/plugins/blog/types.ts +37 -0
  269. package/src/plugins/blog/utils.ts +144 -0
  270. package/src/plugins/client/index.ts +53 -0
  271. package/src/plugins/index.ts +0 -0
  272. package/src/plugins/utils.ts +35 -0
  273. package/src/types.ts +209 -0
  274. package/dist/plugins/index.cjs +0 -15
  275. package/dist/plugins/index.d.cts +0 -64
  276. package/dist/plugins/index.d.mts +0 -64
  277. package/dist/plugins/index.d.ts +0 -64
  278. package/dist/plugins/index.mjs +0 -11
  279. package/dist/shared/stack.DrUAVfIH.d.cts +0 -17
  280. package/dist/shared/stack.DrUAVfIH.d.mts +0 -17
  281. package/dist/shared/stack.DrUAVfIH.d.ts +0 -17
  282. /package/dist/{shared/stack.CktCg4PJ.cjs → plugins/utils.cjs} +0 -0
@@ -0,0 +1,539 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { betterStack } from "../api";
3
+ import { createStackClient } from "../client";
4
+ import { defineBackendPlugin } from "../plugins/api";
5
+ import { defineClientPlugin } from "../plugins/client";
6
+ import type { BackendPlugin, ClientPlugin } from "../types";
7
+ import type { BetterAuthDBSchema, DatabaseDefinition, Adapter } from "@btst/db";
8
+ import { createDbPlugin } from "@btst/db";
9
+ import { createMemoryAdapter } from "@btst/adapter-memory";
10
+ import { createEndpoint as endpoint } from "better-call";
11
+ import { z } from "zod";
12
+ import { createRoute } from "@btst/yar";
13
+
14
+ /**
15
+ * Adapter wrapper for testing
16
+ * Wraps createMemoryAdapter to match the expected signature
17
+ */
18
+ const testAdapter = (db: DatabaseDefinition): Adapter => {
19
+ return createMemoryAdapter(db)({});
20
+ };
21
+
22
+ /**
23
+ * Test schema for messages plugin
24
+ * Using BetterAuthDBSchema format
25
+ */
26
+ const messagesSchema: BetterAuthDBSchema = {
27
+ messages: {
28
+ modelName: "Message",
29
+ fields: {
30
+ id: {
31
+ type: "number",
32
+ unique: true,
33
+ required: true,
34
+ },
35
+ content: {
36
+ type: "string",
37
+ required: true,
38
+ },
39
+ userId: {
40
+ type: "string",
41
+ required: true,
42
+ },
43
+ createdAt: {
44
+ type: "number",
45
+ required: true,
46
+ },
47
+ },
48
+ },
49
+ };
50
+
51
+ /**
52
+ * Test backend plugin - Messages
53
+ * Demonstrates how to build a 3rd party plugin using Better Stack
54
+ * Now using defineBackendPlugin for full type inference - no casts needed!
55
+ */
56
+ const messagesBackendPlugin = defineBackendPlugin({
57
+ name: "messages",
58
+ dbPlugin: createDbPlugin("messages", messagesSchema),
59
+ routes: (adapter) => ({
60
+ list: endpoint(
61
+ "/messages",
62
+ {
63
+ method: "GET",
64
+ query: z.object({
65
+ userId: z.string().optional(),
66
+ }),
67
+ },
68
+ async ({ query }) => {
69
+ const messages = await adapter.findMany({
70
+ model: "messages",
71
+ where: query.userId
72
+ ? [{ field: "userId", value: query.userId, operator: "eq" }]
73
+ : undefined,
74
+ });
75
+ return {
76
+ status: 200,
77
+ body: messages,
78
+ };
79
+ },
80
+ ),
81
+ create: endpoint(
82
+ "/messages",
83
+ {
84
+ method: "POST",
85
+ body: z.object({
86
+ content: z.string().min(1),
87
+ userId: z.string().min(1),
88
+ }),
89
+ },
90
+ async ({ body }) => {
91
+ const message = await adapter.create({
92
+ model: "messages",
93
+ data: { ...body, id: Date.now(), createdAt: Date.now() },
94
+ });
95
+ return {
96
+ status: 201,
97
+ body: message,
98
+ };
99
+ },
100
+ ),
101
+ delete: endpoint(
102
+ "/messages/:id",
103
+ {
104
+ method: "DELETE",
105
+ params: z.object({
106
+ id: z.coerce.number(),
107
+ }),
108
+ },
109
+ async ({ params }) => {
110
+ await adapter.delete({
111
+ model: "messages",
112
+ where: [{ field: "id", value: params.id, operator: "eq" }],
113
+ });
114
+ return {
115
+ status: 204,
116
+ body: null,
117
+ };
118
+ },
119
+ ),
120
+ }),
121
+ });
122
+
123
+ /**
124
+ * Test components for client plugin
125
+ */
126
+ const MessagesListComponent = () => {
127
+ return <div>Messages List</div>;
128
+ };
129
+
130
+ const MessageDetailComponent = () => {
131
+ return <div>Message Detail</div>;
132
+ };
133
+
134
+ /**
135
+ * Test loaders for client plugin
136
+ */
137
+ const messagesListLoader = async () => {
138
+ return { messages: [{ id: 1, content: "Test message" }] };
139
+ };
140
+
141
+ const messageDetailLoader = async ({ params }: { params: { id: string } }) => {
142
+ return { message: { id: params.id, content: "Detail message" } };
143
+ };
144
+
145
+ /**
146
+ * Test client plugin - Messages
147
+ * Now using defineClientPlugin for full type inference - no casts needed!
148
+ * Using Yar's createRoute() to create proper route handlers.
149
+ */
150
+ const messagesClientPlugin = defineClientPlugin({
151
+ name: "messages",
152
+ routes: () => ({
153
+ messagesList: createRoute("/messages", () => ({
154
+ PageComponent: MessagesListComponent,
155
+ loader: messagesListLoader,
156
+ })),
157
+ messageDetail: createRoute("/messages/:id", ({ params }) => ({
158
+ PageComponent: MessageDetailComponent,
159
+ loader: () => messageDetailLoader({ params }),
160
+ })),
161
+ }),
162
+ });
163
+
164
+ describe("3rd Party Plugin Support", () => {
165
+ describe("Backend Plugin", () => {
166
+ it("should create backend with custom plugin", () => {
167
+ const backend = betterStack({
168
+ basePath: "/api",
169
+ plugins: {
170
+ messages: messagesBackendPlugin,
171
+ },
172
+ adapter: testAdapter,
173
+ });
174
+
175
+ expect(backend).toBeDefined();
176
+ expect(backend.handler).toBeDefined();
177
+ expect(backend.router).toBeDefined();
178
+ expect(backend.dbSchema).toBeDefined();
179
+ });
180
+
181
+ it("should create backend with multiple custom plugins", () => {
182
+ // Create a second plugin
183
+ const notificationsSchema: BetterAuthDBSchema = {
184
+ notifications: {
185
+ modelName: "Notification",
186
+ fields: {
187
+ id: {
188
+ type: "number",
189
+ unique: true,
190
+ required: true,
191
+ },
192
+ message: {
193
+ type: "string",
194
+ required: true,
195
+ },
196
+ },
197
+ },
198
+ };
199
+
200
+ const notificationsPlugin = defineBackendPlugin({
201
+ name: "notifications",
202
+ dbPlugin: createDbPlugin("notifications", notificationsSchema),
203
+ routes: (adapter) => ({
204
+ list: endpoint(
205
+ "/notifications",
206
+ {
207
+ method: "GET",
208
+ },
209
+ async () => {
210
+ const notifications = await adapter.findMany({
211
+ model: "notifications",
212
+ });
213
+ return {
214
+ status: 200,
215
+ body: notifications,
216
+ };
217
+ },
218
+ ),
219
+ }),
220
+ });
221
+
222
+ const backend = betterStack({
223
+ basePath: "/api",
224
+ plugins: {
225
+ messages: messagesBackendPlugin,
226
+ notifications: notificationsPlugin,
227
+ },
228
+ adapter: testAdapter,
229
+ });
230
+
231
+ expect(backend).toBeDefined();
232
+ expect(backend.router).toBeDefined();
233
+ });
234
+
235
+ it("should generate routes for custom plugin", () => {
236
+ const backend = betterStack({
237
+ basePath: "/api",
238
+ plugins: {
239
+ messages: messagesBackendPlugin,
240
+ },
241
+ adapter: testAdapter,
242
+ });
243
+
244
+ // Check that routes are properly registered
245
+ // The router should have routes prefixed with plugin name
246
+ expect(backend.router).toBeDefined();
247
+ expect(typeof backend.router.handler).toBe("function");
248
+ });
249
+
250
+ it("should include plugin schema in database", () => {
251
+ const backend = betterStack({
252
+ basePath: "/api",
253
+ plugins: {
254
+ messages: messagesBackendPlugin,
255
+ },
256
+ adapter: testAdapter,
257
+ });
258
+
259
+ // Check that the schema includes the messages table
260
+ expect(backend.dbSchema).toBeDefined();
261
+ expect(backend.dbSchema.schema).toBeDefined();
262
+ expect(backend.dbSchema.schema.messages).toBeDefined();
263
+ });
264
+ });
265
+
266
+ describe("Client Plugin", () => {
267
+ it("should create client with custom plugin", () => {
268
+ const client = createStackClient({
269
+ plugins: {
270
+ messages: messagesClientPlugin,
271
+ },
272
+ });
273
+
274
+ expect(client).toBeDefined();
275
+ expect(client.router).toBeDefined();
276
+ });
277
+
278
+ it("should have routes from custom plugin", () => {
279
+ const client = createStackClient({
280
+ plugins: {
281
+ messages: messagesClientPlugin,
282
+ },
283
+ });
284
+
285
+ // Verify the router is created and has the expected structure
286
+ expect(client.router).toBeDefined();
287
+ expect(typeof client.router).toBe("object");
288
+ });
289
+
290
+ it("should return correct route shape with Component and loader", async () => {
291
+ // Test the route object directly from the plugin - types are now preserved!
292
+ const routes = messagesClientPlugin.routes();
293
+ const listRoute = routes.messagesList;
294
+
295
+ expect(listRoute).toBeDefined();
296
+ expect(typeof listRoute).toBe("function"); // Yar routes ARE functions
297
+ expect(listRoute.path).toBe("/messages");
298
+
299
+ // Call the route handler to get the route data
300
+ const routeData = listRoute();
301
+ expect(routeData).toBeDefined();
302
+ // Handler returns PageComponent
303
+ expect(routeData.PageComponent).toBeDefined();
304
+ expect(typeof routeData.PageComponent).toBe("function");
305
+ expect(routeData.loader).toBeDefined();
306
+ expect(typeof routeData.loader).toBe("function");
307
+
308
+ // Verify PageComponent returns expected shape
309
+ expect(routeData.PageComponent).toBeDefined();
310
+
311
+ // Verify loader returns expected shape
312
+ const data = await routeData.loader?.();
313
+ expect(data).toBeDefined();
314
+ expect(data?.messages).toBeDefined();
315
+ expect(Array.isArray(data?.messages)).toBe(true);
316
+ });
317
+
318
+ it("should return correct route shape with params", async () => {
319
+ // Test the route object directly from the plugin - types are now preserved!
320
+ const routes = messagesClientPlugin.routes();
321
+ const detailRoute = routes.messageDetail;
322
+
323
+ expect(detailRoute).toBeDefined();
324
+ expect(typeof detailRoute).toBe("function"); // Yar routes ARE functions
325
+ expect(detailRoute.path).toBe("/messages/:id");
326
+
327
+ // Call the route handler with params to get the route data
328
+ const routeData = detailRoute({ params: { id: "123" } });
329
+ expect(routeData).toBeDefined();
330
+ // Handler returns PageComponent
331
+ expect(routeData.PageComponent).toBeDefined();
332
+ expect(typeof routeData.PageComponent).toBe("function");
333
+ expect(routeData.loader).toBeDefined();
334
+ expect(typeof routeData.loader).toBeDefined();
335
+
336
+ // Verify loader can be called
337
+ const data = await routeData.loader?.();
338
+ expect(data).toBeDefined();
339
+ expect(data?.message).toBeDefined();
340
+ expect(data?.message.id).toBe("123");
341
+ });
342
+
343
+ it("should have all expected routes defined", () => {
344
+ const routes = messagesClientPlugin.routes();
345
+
346
+ // Verify all expected routes exist - types are fully inferred!
347
+ expect(routes.messagesList).toBeDefined();
348
+ expect(routes.messageDetail).toBeDefined();
349
+
350
+ // Verify route paths (Yar routes have .path property)
351
+ expect(routes.messagesList.path).toBe("/messages");
352
+ expect(routes.messageDetail.path).toBe("/messages/:id");
353
+ });
354
+
355
+ it("should maintain type safety across route definitions", async () => {
356
+ // This test verifies that TypeScript types are preserved
357
+ const routes = messagesClientPlugin.routes();
358
+ const route = routes.messagesList;
359
+
360
+ // Route should be a function (Yar's handler)
361
+ expect(typeof route).toBe("function");
362
+
363
+ // Calling route handler returns route data
364
+ const routeData = route();
365
+ // Handler returns PageComponent
366
+ expect(routeData.PageComponent).toBeDefined();
367
+
368
+ // PageComponent should be callable
369
+ expect(routeData.PageComponent).toBeDefined();
370
+
371
+ // Loader should be callable and return a promise
372
+ const loaderResult = routeData.loader?.();
373
+ expect(loaderResult).toBeInstanceOf(Promise);
374
+ await loaderResult; // Ensure it resolves
375
+ });
376
+
377
+ it("should support multiple client plugins", () => {
378
+ const NotificationsComponent: React.FC = () => {
379
+ return <div>Notifications</div>;
380
+ };
381
+
382
+ const notificationsLoader = async () => {
383
+ return { notifications: [{ id: 1, message: "Test notification" }] };
384
+ };
385
+
386
+ const notificationsClientPlugin = defineClientPlugin({
387
+ name: "notifications",
388
+ routes: () => ({
389
+ notificationsList: createRoute("/notifications", () => ({
390
+ PageComponent: NotificationsComponent,
391
+ loader: notificationsLoader,
392
+ })),
393
+ }),
394
+ });
395
+
396
+ const client = createStackClient({
397
+ plugins: {
398
+ messages: messagesClientPlugin,
399
+ notifications: notificationsClientPlugin,
400
+ },
401
+ });
402
+
403
+ // Verify the router is created with both plugins
404
+ expect(client.router).toBeDefined();
405
+
406
+ // Verify both plugin routes are accessible from their definitions - full type safety!
407
+ const messagesRoutes = messagesClientPlugin.routes();
408
+ const notificationsRoutes = notificationsClientPlugin.routes();
409
+
410
+ expect(messagesRoutes.messagesList).toBeDefined();
411
+ expect(typeof messagesRoutes.messagesList).toBe("function");
412
+ expect(messagesRoutes.messagesList.path).toBe("/messages");
413
+
414
+ expect(notificationsRoutes.notificationsList).toBeDefined();
415
+ expect(typeof notificationsRoutes.notificationsList).toBe("function");
416
+ expect(notificationsRoutes.notificationsList.path).toBe("/notifications");
417
+ });
418
+ });
419
+
420
+ describe("Separate Backend and Client Plugins", () => {
421
+ it("should use backend and client plugins independently", () => {
422
+ // Backend and client plugins are completely separate
423
+ // This prevents SSR issues and enables better code splitting
424
+ const backend = betterStack({
425
+ basePath: "/api",
426
+ plugins: {
427
+ messages: messagesBackendPlugin,
428
+ },
429
+ adapter: testAdapter,
430
+ });
431
+
432
+ const client = createStackClient({
433
+ plugins: {
434
+ messages: messagesClientPlugin,
435
+ },
436
+ });
437
+
438
+ expect(backend.router).toBeDefined();
439
+ expect(client.router).toBeDefined();
440
+ });
441
+
442
+ it("should demonstrate proper route usage pattern", async () => {
443
+ // This test mimics the actual usage pattern in a Next.js app
444
+ // Uses router.getRoute() to match paths and get route handlers
445
+
446
+ const client = createStackClient({
447
+ plugins: {
448
+ messages: messagesClientPlugin,
449
+ },
450
+ });
451
+
452
+ // Test route resolution by path
453
+ const matchedRoute = client.router.getRoute("/messages");
454
+
455
+ expect(matchedRoute).toBeDefined();
456
+ expect(matchedRoute).not.toBeNull();
457
+
458
+ if (matchedRoute) {
459
+ // Route should have the expected properties from Yar
460
+ expect(matchedRoute.params).toBeDefined();
461
+
462
+ // Yar's getRoute() returns the route handler's result
463
+ // which includes PageComponent and loader
464
+ expect(matchedRoute.PageComponent).toBeDefined();
465
+ expect(typeof matchedRoute.PageComponent).toBe("function");
466
+
467
+ // Can render the component
468
+ if (matchedRoute.PageComponent) {
469
+ const rendered = <matchedRoute.PageComponent />;
470
+ expect(rendered).toBeDefined();
471
+ }
472
+
473
+ // Verify loader is present and callable
474
+ expect(matchedRoute.loader).toBeDefined();
475
+ expect(typeof matchedRoute.loader).toBe("function");
476
+
477
+ // Can call the loader and get data - type is inferred from loader
478
+ const data = await matchedRoute.loader?.();
479
+ expect(data).toBeDefined();
480
+
481
+ // TypeScript knows this is a union of all route loaders
482
+ // Narrow by checking which properties exist
483
+ if (data && "messages" in data) {
484
+ // This is the messagesList route
485
+ expect(data.messages).toBeDefined();
486
+ expect(Array.isArray(data.messages)).toBe(true);
487
+ }
488
+ }
489
+ });
490
+
491
+ it("should handle parameterized routes correctly", async () => {
492
+ // Test that router.getRoute() correctly extracts path parameters
493
+ const client = createStackClient({
494
+ plugins: {
495
+ messages: messagesClientPlugin,
496
+ },
497
+ });
498
+
499
+ // Match a route with params
500
+ const matchedRoute = client.router.getRoute("/messages/42");
501
+
502
+ expect(matchedRoute).toBeDefined();
503
+ expect(matchedRoute).not.toBeNull();
504
+
505
+ if (matchedRoute) {
506
+ // Yar should extract params from the path
507
+ expect(matchedRoute.params).toBeDefined();
508
+ expect(matchedRoute.params.id).toBe("42");
509
+
510
+ // getRoute() returns the handler result with PageComponent and loader
511
+ expect(matchedRoute.PageComponent).toBeDefined();
512
+ expect(matchedRoute.loader).toBeDefined();
513
+
514
+ // Loader should be callable - type is properly inferred from the route
515
+ const data = await matchedRoute.loader?.();
516
+ expect(data).toBeDefined();
517
+
518
+ // TypeScript knows this is a union type from both routes
519
+ // We can narrow by checking which properties exist
520
+ if (data && "message" in data) {
521
+ // This is the messageDetail route
522
+ expect(data.message).toBeDefined();
523
+ expect(data.message.id).toBe("42");
524
+ }
525
+ }
526
+ });
527
+ });
528
+
529
+ describe("Plugin Type Exports", () => {
530
+ it("should export all necessary types for building plugins", () => {
531
+ // This test verifies the types are available at compile time
532
+ const backendPlugin: BackendPlugin = messagesBackendPlugin;
533
+ const clientPlugin: ClientPlugin = messagesClientPlugin;
534
+
535
+ expect(backendPlugin.name).toBe("messages");
536
+ expect(clientPlugin.name).toBe("messages");
537
+ });
538
+ });
539
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createStackClient } from "../client";
3
+ import { defineClientPlugin } from "../plugins/client";
4
+ import { createRoute } from "@btst/yar";
5
+
6
+ describe("Client sitemap generation", () => {
7
+ it("aggregates sitemaps from multiple plugins and de-duplicates by URL", async () => {
8
+ const pluginA = defineClientPlugin({
9
+ name: "a",
10
+ routes: () => ({
11
+ a: createRoute("/a", () => ({ PageComponent: () => null })),
12
+ }),
13
+ sitemap: () => [
14
+ { url: "https://example.com/a", priority: 0.8 },
15
+ { url: "https://example.com/b", changeFrequency: "weekly" },
16
+ ],
17
+ });
18
+
19
+ const pluginB = defineClientPlugin({
20
+ name: "b",
21
+ routes: () => ({
22
+ b: createRoute("/b", () => ({ PageComponent: () => null })),
23
+ }),
24
+ sitemap: async () => [
25
+ { url: "https://example.com/b", priority: 0.5 }, // duplicate
26
+ { url: "https://example.com/c", changeFrequency: "monthly" },
27
+ ],
28
+ });
29
+
30
+ const client = createStackClient({
31
+ plugins: { a: pluginA, b: pluginB },
32
+ });
33
+
34
+ const entries = await client.generateSitemap();
35
+ const urls = entries.map((e) => e.url).sort();
36
+ expect(urls).toEqual([
37
+ "https://example.com/a",
38
+ "https://example.com/b",
39
+ "https://example.com/c",
40
+ ]);
41
+
42
+ // Ensure de-duplication preserves first occurrence properties
43
+ const bEntry = entries.find((e) => e.url === "https://example.com/b");
44
+ expect(bEntry?.changeFrequency).toBe("weekly");
45
+ expect(bEntry?.priority).toBeUndefined();
46
+ });
47
+
48
+ it("returns empty array when no plugins provide a sitemap", async () => {
49
+ const noop = defineClientPlugin({
50
+ name: "noop",
51
+ routes: () => ({
52
+ home: createRoute("/", () => ({ PageComponent: () => null })),
53
+ }),
54
+ });
55
+
56
+ const client = createStackClient({ plugins: { noop } });
57
+ const entries = await client.generateSitemap();
58
+ expect(entries).toEqual([]);
59
+ });
60
+ });
@@ -0,0 +1,75 @@
1
+ import { createRouter } from "better-call";
2
+ import type {
3
+ BackendLibConfig,
4
+ BackendLib,
5
+ PrefixedPluginRoutes,
6
+ } from "../types";
7
+ import { defineDb } from "@btst/db";
8
+
9
+ export { toNodeHandler } from "better-call/node";
10
+
11
+ /**
12
+ * Creates the backend library with plugin support
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const api = betterStack({
17
+ * plugins: {
18
+ * messages: messagesPlugin.backend
19
+ * },
20
+ * adapter: memoryAdapter
21
+ * });
22
+ *
23
+ * // Use in API route:
24
+ * export const GET = api.handler;
25
+ * export const POST = api.handler;
26
+ * ```
27
+ *
28
+ * @template TPlugins - The exact plugins map (inferred from config)
29
+ * @template TRoutes - All routes with prefixed keys like "pluginName_routeName" (computed automatically)
30
+ */
31
+ export function betterStack<
32
+ TPlugins extends Record<string, any>,
33
+ TRoutes extends
34
+ PrefixedPluginRoutes<TPlugins> = PrefixedPluginRoutes<TPlugins>,
35
+ >(config: BackendLibConfig<TPlugins>): BackendLib<TRoutes> {
36
+ const { plugins, adapter, dbSchema, basePath } = config;
37
+
38
+ // Collect all routes from all plugins with type-safe prefixed keys
39
+ const allRoutes = {} as TRoutes;
40
+
41
+ let betterDbSchema = dbSchema ?? defineDb({});
42
+
43
+ // use all the db plugins on the betterDbSchema
44
+ for (const [pluginKey, plugin] of Object.entries(plugins)) {
45
+ betterDbSchema = betterDbSchema.use(plugin.dbPlugin);
46
+ }
47
+
48
+ for (const [pluginKey, plugin] of Object.entries(plugins)) {
49
+ // Pass the adapter directly to plugin routes
50
+ const pluginRoutes = plugin.routes(adapter(betterDbSchema));
51
+
52
+ // Prefix route keys with plugin name to avoid collisions
53
+ for (const [routeKey, endpoint] of Object.entries(pluginRoutes)) {
54
+ const compositeKey = `${pluginKey}_${routeKey}` as keyof TRoutes;
55
+ (allRoutes as any)[compositeKey] = endpoint;
56
+ }
57
+ }
58
+
59
+ // Create the composed router
60
+ const router = createRouter(allRoutes, {
61
+ basePath: basePath,
62
+ });
63
+
64
+ return {
65
+ handler: router.handler,
66
+ router,
67
+ dbSchema: betterDbSchema,
68
+ };
69
+ }
70
+
71
+ export type {
72
+ BackendPlugin,
73
+ BackendLibConfig,
74
+ BackendLib,
75
+ } from "../types";