dineway 0.1.17 → 0.1.19

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 (212) hide show
  1. package/dist/api/route-utils.d.mts +2 -2
  2. package/dist/api/route-utils.mjs +3 -3
  3. package/dist/api/schemas/index.d.mts +2 -2
  4. package/dist/api/schemas/index.mjs +4 -4
  5. package/dist/{api-CgxdfUg4.mjs → api-CUdFqNOK.mjs} +11 -11
  6. package/dist/{apply-CGVHi2r7.mjs → apply-Wyg77UOb.mjs} +13 -13
  7. package/dist/astro/index.d.mts +2 -2
  8. package/dist/astro/index.mjs +5 -1
  9. package/dist/astro/middleware/auth.d.mts +2 -2
  10. package/dist/astro/middleware/redirect.mjs +3 -3
  11. package/dist/astro/middleware.mjs +43 -43
  12. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +3 -3
  13. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +3 -3
  14. package/dist/astro/routes/api/admin/briefing.mjs +10 -10
  15. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +6 -6
  16. package/dist/astro/routes/api/admin/bylines/index.mjs +6 -6
  17. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +3 -3
  18. package/dist/astro/routes/api/admin/comments/bulk.mjs +3 -3
  19. package/dist/astro/routes/api/admin/comments/index.mjs +3 -3
  20. package/dist/astro/routes/api/admin/context/_id_/history.mjs +13 -13
  21. package/dist/astro/routes/api/admin/context/_id_/index.mjs +13 -13
  22. package/dist/astro/routes/api/admin/context/_id_/review.mjs +15 -15
  23. package/dist/astro/routes/api/admin/context/_id_/supersede.mjs +15 -15
  24. package/dist/astro/routes/api/admin/context/diff.mjs +15 -15
  25. package/dist/astro/routes/api/admin/context/index.mjs +15 -15
  26. package/dist/astro/routes/api/admin/context/stale.mjs +15 -15
  27. package/dist/astro/routes/api/admin/hitl-requests/_id_/index.mjs +14 -14
  28. package/dist/astro/routes/api/admin/hitl-requests/_id_/resolve.mjs +16 -16
  29. package/dist/astro/routes/api/admin/hitl-requests/index.mjs +16 -16
  30. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +13 -13
  31. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +29 -29
  32. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +29 -29
  33. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +26 -26
  34. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +29 -29
  35. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +29 -29
  36. package/dist/astro/routes/api/admin/plugins/index.mjs +26 -26
  37. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +26 -26
  38. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +29 -29
  39. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +26 -26
  40. package/dist/astro/routes/api/admin/plugins/updates.mjs +26 -26
  41. package/dist/astro/routes/api/admin/review-requests/_id_/index.mjs +1 -1
  42. package/dist/astro/routes/api/admin/review-requests/_id_/resolve.mjs +15 -15
  43. package/dist/astro/routes/api/admin/review-requests/index.mjs +4 -4
  44. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +26 -26
  45. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +26 -26
  46. package/dist/astro/routes/api/admin/users/_id_/index.mjs +3 -3
  47. package/dist/astro/routes/api/admin/users/index.mjs +3 -3
  48. package/dist/astro/routes/api/auth/invite/complete.mjs +3 -3
  49. package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
  50. package/dist/astro/routes/api/auth/invite/register-options.mjs +3 -3
  51. package/dist/astro/routes/api/auth/magic-link/send.mjs +3 -3
  52. package/dist/astro/routes/api/auth/me.mjs +3 -3
  53. package/dist/astro/routes/api/auth/passkey/_id_.mjs +3 -3
  54. package/dist/astro/routes/api/auth/passkey/options.mjs +3 -3
  55. package/dist/astro/routes/api/auth/passkey/register/options.mjs +3 -3
  56. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +3 -3
  57. package/dist/astro/routes/api/auth/passkey/verify.mjs +3 -3
  58. package/dist/astro/routes/api/auth/signup/complete.mjs +3 -3
  59. package/dist/astro/routes/api/auth/signup/request.mjs +3 -3
  60. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +3 -3
  61. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +1 -1
  62. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +1 -1
  63. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +1 -1
  64. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +3 -3
  65. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +15 -15
  66. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +1 -1
  67. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +5 -5
  68. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +7 -7
  69. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +1 -1
  70. package/dist/astro/routes/api/content/_collection_/_id_.mjs +5 -5
  71. package/dist/astro/routes/api/content/_collection_/index.mjs +4 -4
  72. package/dist/astro/routes/api/content/_collection_/trash.mjs +3 -3
  73. package/dist/astro/routes/api/dashboard.mjs +1 -1
  74. package/dist/astro/routes/api/health.mjs +1 -1
  75. package/dist/astro/routes/api/import/probe.mjs +5 -5
  76. package/dist/astro/routes/api/import/wordpress/analyze.mjs +1 -1
  77. package/dist/astro/routes/api/import/wordpress/execute.mjs +15 -15
  78. package/dist/astro/routes/api/import/wordpress/media.mjs +4 -4
  79. package/dist/astro/routes/api/import/wordpress/prepare.mjs +19 -19
  80. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +17 -17
  81. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +5 -5
  82. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +18 -18
  83. package/dist/astro/routes/api/manifest.mjs +1 -1
  84. package/dist/astro/routes/api/mcp.mjs +38 -38
  85. package/dist/astro/routes/api/media/_id_/confirm.mjs +3 -3
  86. package/dist/astro/routes/api/media/_id_.mjs +3 -3
  87. package/dist/astro/routes/api/media/upload-url.mjs +5 -5
  88. package/dist/astro/routes/api/media.mjs +7 -7
  89. package/dist/astro/routes/api/menus/_name_/items/_id_.d.mts +8 -0
  90. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +127 -0
  91. package/dist/astro/routes/api/menus/_name_/items.d.mts +1 -3
  92. package/dist/astro/routes/api/menus/_name_/items.mjs +17 -108
  93. package/dist/astro/routes/api/menus/_name_/reorder.mjs +16 -16
  94. package/dist/astro/routes/api/menus/_name_/translations.mjs +16 -16
  95. package/dist/astro/routes/api/menus/_name_.mjs +16 -16
  96. package/dist/astro/routes/api/menus/index.mjs +16 -16
  97. package/dist/astro/routes/api/openapi.json.mjs +26 -26
  98. package/dist/astro/routes/api/redirects/404s/index.mjs +5 -5
  99. package/dist/astro/routes/api/redirects/404s/summary.mjs +5 -5
  100. package/dist/astro/routes/api/redirects/_id_.mjs +17 -17
  101. package/dist/astro/routes/api/redirects/index.mjs +17 -17
  102. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +26 -26
  103. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +26 -26
  104. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +26 -26
  105. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +26 -26
  106. package/dist/astro/routes/api/schema/collections/index.mjs +26 -26
  107. package/dist/astro/routes/api/schema/index.mjs +3 -3
  108. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +26 -26
  109. package/dist/astro/routes/api/schema/orphans/index.mjs +26 -26
  110. package/dist/astro/routes/api/search/enable.mjs +6 -6
  111. package/dist/astro/routes/api/search/index.mjs +6 -6
  112. package/dist/astro/routes/api/search/rebuild.mjs +6 -6
  113. package/dist/astro/routes/api/search/stats.mjs +3 -3
  114. package/dist/astro/routes/api/search/suggest.mjs +6 -6
  115. package/dist/astro/routes/api/sections/_slug_.mjs +16 -16
  116. package/dist/astro/routes/api/sections/index.mjs +16 -16
  117. package/dist/astro/routes/api/settings.mjs +16 -16
  118. package/dist/astro/routes/api/setup/admin-verify.mjs +3 -3
  119. package/dist/astro/routes/api/setup/admin.mjs +3 -3
  120. package/dist/astro/routes/api/setup/dev-bypass.mjs +11 -11
  121. package/dist/astro/routes/api/setup/index.mjs +14 -14
  122. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +18 -18
  123. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +18 -18
  124. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +18 -18
  125. package/dist/astro/routes/api/taxonomies/index.mjs +18 -18
  126. package/dist/astro/routes/api/typegen.mjs +3 -3
  127. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  128. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +15 -15
  129. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +16 -16
  130. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +16 -16
  131. package/dist/astro/routes/api/widget-areas/_name_.mjs +14 -14
  132. package/dist/astro/routes/api/widget-areas/index.mjs +16 -16
  133. package/dist/astro/routes/robots.txt.mjs +3 -3
  134. package/dist/astro/routes/sitemap-_collection_.xml.mjs +4 -4
  135. package/dist/astro/routes/sitemap.xml.mjs +4 -4
  136. package/dist/astro/types.d.mts +2 -2
  137. package/dist/{briefing-4k6R0KAr.mjs → briefing-Dk4I4VC8.mjs} +3 -3
  138. package/dist/{briefing-DM7e2yd3.mjs → briefing-PFT3T6KW.mjs} +1 -1
  139. package/dist/{byline-BxAf79s1.mjs → byline-9WeA8b0a.mjs} +1 -1
  140. package/dist/{bylines-DhCnhR70.mjs → bylines-B6QpwVAr.mjs} +2 -2
  141. package/dist/{bylines--KurdDQK.d.mts → bylines-CCkI7Rh3.d.mts} +76 -65
  142. package/dist/{cache-inQvbrP5.mjs → cache-BhJYDPP8.mjs} +1 -1
  143. package/dist/cli/index.mjs +11 -11
  144. package/dist/client/external-auth-headers.d.mts +1 -1
  145. package/dist/client/index.d.mts +1 -1
  146. package/dist/{context-CT44ux1O.mjs → context-DRq-f4sM.mjs} +2 -2
  147. package/dist/{context-CGbVhAmd.mjs → context-oSCcgE1U.mjs} +2 -2
  148. package/dist/{hitl-requests-CUnch_4e.mjs → hitl-requests-CJBs03Ep.mjs} +1 -1
  149. package/dist/{hitl-route-helpers-DcUCm57p.mjs → hitl-route-helpers-kTkP1NJ7.mjs} +1 -1
  150. package/dist/{import-BVqG28WP.mjs → import-B8lnqfnN.mjs} +1 -1
  151. package/dist/{import-DE2ziezs.mjs → import-BO_gy5vZ.mjs} +2 -2
  152. package/dist/index.d.mts +2 -2
  153. package/dist/index.mjs +41 -41
  154. package/dist/{jsonld-BjsoTL5m.d.mts → jsonld-CvXmIBls.d.mts} +2 -2
  155. package/dist/media/index.mjs +2 -2
  156. package/dist/media/local-runtime.d.mts +2 -2
  157. package/dist/{media-allowlist-eS3AZ2L_.mjs → media-allowlist-vdlqBR-8.mjs} +1 -1
  158. package/dist/{menus-BDW1yIdj.mjs → menus-BTOa-tsI.mjs} +3 -3
  159. package/dist/menus-DXaFTRyU.mjs +720 -0
  160. package/dist/page/index.d.mts +1 -1
  161. package/dist/page/index.mjs +1 -1
  162. package/dist/plugins/adapt-sandbox-entry.d.mts +2 -2
  163. package/dist/{plugins-DKceDtUM.mjs → plugins-CxUx-b5a.mjs} +1 -1
  164. package/dist/{query-BtuwuZRd.mjs → query-CH1GaJmt.mjs} +3 -3
  165. package/dist/{query-CWPxZjGM.mjs → query-TQqoOaxy.mjs} +7 -7
  166. package/dist/{redirect-BHo9--Jz.mjs → redirect-hKO66LS_.mjs} +1 -1
  167. package/dist/{redirects-xVDvj_yQ.mjs → redirects-BO0fI750.mjs} +30 -22
  168. package/dist/{redirects-Bc40BgxA.mjs → redirects-CvIzUvek.mjs} +3 -3
  169. package/dist/{registry-DumDzFDn.mjs → registry-CYO6XQ-4.mjs} +3 -3
  170. package/dist/{runtime-B5hJAnKF.d.mts → runtime-DY_XmE9Y.d.mts} +2 -2
  171. package/dist/runtime.d.mts +3 -3
  172. package/dist/runtime.mjs +1 -1
  173. package/dist/{schema-D1z41cq_.mjs → schema-mjeMcn_6.mjs} +1 -1
  174. package/dist/{search-CPcQGTHW.mjs → search-vzCYAk0F.mjs} +2 -2
  175. package/dist/{sections-B9RYyf3I.mjs → sections-DA3GMhNG.mjs} +1 -1
  176. package/dist/seed/index.mjs +11 -11
  177. package/dist/{seo-BPz1KkCq.mjs → seo-DASNc4gD.mjs} +1 -1
  178. package/dist/{seo-contributions-CQzUjJKY.mjs → seo-contributions-BG4TtU7H.mjs} +5 -5
  179. package/dist/{settings-BGCo_c2y.mjs → settings-BJW_lmrM.mjs} +1 -1
  180. package/dist/{settings-qPzY2KIF.mjs → settings-D2k1JraC.mjs} +13 -2
  181. package/dist/{site-context-DHRIU6x9.mjs → site-context-CyBvmvQY.mjs} +7 -7
  182. package/dist/{ssrf-BOSGjXxb.mjs → ssrf-z3oH8wjK.mjs} +4 -4
  183. package/dist/{taxonomies-aQXvYVPm.mjs → taxonomies-CagluJHp.mjs} +2 -2
  184. package/dist/{taxonomies-BWmxbumf.mjs → taxonomies-DPNIwTGk.mjs} +4 -4
  185. package/dist/{taxonomy-DI-0HgKe.mjs → taxonomy-DpXdVSSR.mjs} +1 -1
  186. package/dist/ui/server-runtime.d.mts +6 -3
  187. package/dist/ui/server-runtime.mjs +53 -12
  188. package/dist/{validate-B8ZvLeOE.mjs → validate-CaKEnLed.mjs} +1 -1
  189. package/dist/version-CGRX_5CT.mjs +6 -0
  190. package/dist/{widgets-Cce2-2fM.mjs → widgets-D7UK-2hW.mjs} +1 -1
  191. package/dist/{wordpress-slugs-C4EhE6fo.mjs → wordpress-slugs-Bnf3-sf8.mjs} +1 -1
  192. package/dist/{zod-generator-BKhtHT3e.mjs → zod-generator-vOxhed6n.mjs} +1 -1
  193. package/package.json +7 -6
  194. package/src/components/DinewayHead.astro +11 -3
  195. package/dist/menus-CwI7DQJ5.mjs +0 -569
  196. package/dist/version-CjsyxeBD.mjs +0 -6
  197. /package/dist/{chunks-vkrCT4Ta.mjs → chunks-BjOMG4LF.mjs} +0 -0
  198. /package/dist/{context-types-DuiyY6xF.mjs → context-types-BWspNMDr.mjs} +0 -0
  199. /package/dist/{dashboard-6gH7SlPM.mjs → dashboard-DdqRifyu.mjs} +0 -0
  200. /package/dist/{fts-manager-D7KVeBmg.mjs → fts-manager-DYRy6HVi.mjs} +0 -0
  201. /package/dist/{hash-V8oZo1W2.mjs → hash-DHSsP6_G.mjs} +0 -0
  202. /package/dist/{loader-CQFxFOt1.mjs → loader-dt5DoyI1.mjs} +0 -0
  203. /package/dist/{mime-DJOFDGux.mjs → mime-DltzYYAL.mjs} +0 -0
  204. /package/dist/{normalize-9GU-bv_T.mjs → normalize-ba36HTxZ.mjs} +0 -0
  205. /package/dist/{patterns-CW4n2PQs.mjs → patterns-BKmjvM7K.mjs} +0 -0
  206. /package/dist/{placeholder-BJPmhjHP.mjs → placeholder-BAy3k441.mjs} +0 -0
  207. /package/dist/{provider-loader-D0F6E2qv.d.mts → provider-loader-BPhii_3H.d.mts} +0 -0
  208. /package/dist/{request-cache-DmVyQUBh.mjs → request-cache-BzuhyUXj.mjs} +0 -0
  209. /package/dist/{review-requests-vCw7_3DS.mjs → review-requests-CO-vO0O0.mjs} +0 -0
  210. /package/dist/{seo-DJoop90w.mjs → seo-Dl4QE4El.mjs} +0 -0
  211. /package/dist/{transport-CyOHECBA.d.mts → transport-DA3H3xB4.d.mts} +0 -0
  212. /package/dist/{types-Dz2EKzsX.mjs → types-zfg8SDVI.mjs} +0 -0
@@ -0,0 +1,720 @@
1
+ import { t as __exportAll } from "./chunk-ClPoSABd.mjs";
2
+ import { n as getI18nConfig } from "./config-XW5tMrH8.mjs";
3
+ import { t as withTransaction } from "./transaction-qfqpPVpu.mjs";
4
+ import { ulid } from "ulidx";
5
+
6
+ //#region src/database/repositories/menu.ts
7
+ /**
8
+ * Thrown from inside a repository transaction when the menu the caller
9
+ * resolved earlier has since been deleted. Handlers translate this to a
10
+ * `NOT_FOUND` API response. Necessary because D1 disables FK enforcement
11
+ * (so `ON DELETE CASCADE` won't fire), and an unchecked `setItems` would
12
+ * happily insert items whose `menu_id` no longer exists, leaving orphans.
13
+ */
14
+ var MenuGoneError = class extends Error {
15
+ constructor(menuId) {
16
+ super(`Menu ${menuId} was deleted while being modified`);
17
+ this.menuId = menuId;
18
+ this.name = "MenuGoneError";
19
+ }
20
+ };
21
+ function rowToMenu(row) {
22
+ return {
23
+ id: row.id,
24
+ name: row.name,
25
+ label: row.label,
26
+ createdAt: row.created_at,
27
+ updatedAt: row.updated_at,
28
+ locale: row.locale,
29
+ translationGroup: row.translation_group
30
+ };
31
+ }
32
+ function rowToMenuItem(row) {
33
+ return {
34
+ id: row.id,
35
+ menuId: row.menu_id,
36
+ parentId: row.parent_id,
37
+ sortOrder: row.sort_order,
38
+ type: row.type,
39
+ referenceCollection: row.reference_collection,
40
+ referenceId: row.reference_id,
41
+ customUrl: row.custom_url,
42
+ label: row.label,
43
+ titleAttr: row.title_attr,
44
+ target: row.target,
45
+ cssClasses: row.css_classes,
46
+ createdAt: row.created_at,
47
+ locale: row.locale,
48
+ translationGroup: row.translation_group
49
+ };
50
+ }
51
+ var MenuRepository = class {
52
+ constructor(db) {
53
+ this.db = db;
54
+ }
55
+ /**
56
+ * List menus with their item counts. When `locale` is omitted, returns
57
+ * every locale variant as its own row (consistent with the admin listing
58
+ * model: each translation is its own menu for editing purposes).
59
+ */
60
+ async findMany(options = {}) {
61
+ let query = this.db.selectFrom("_dineway_menus as m").leftJoin("_dineway_menu_items as i", "i.menu_id", "m.id").select(({ fn }) => [
62
+ "m.id",
63
+ "m.name",
64
+ "m.label",
65
+ "m.created_at",
66
+ "m.updated_at",
67
+ "m.locale",
68
+ "m.translation_group",
69
+ fn.count("i.id").as("itemCount")
70
+ ]).groupBy([
71
+ "m.id",
72
+ "m.name",
73
+ "m.label",
74
+ "m.created_at",
75
+ "m.updated_at",
76
+ "m.locale",
77
+ "m.translation_group"
78
+ ]).orderBy("m.name", "asc");
79
+ if (options.locale !== void 0) query = query.where("m.locale", "=", options.locale);
80
+ return (await query.execute()).map((row) => ({
81
+ itemCount: typeof row.itemCount === "string" ? Number(row.itemCount) : row.itemCount,
82
+ ...rowToMenu({
83
+ id: row.id,
84
+ name: row.name,
85
+ label: row.label,
86
+ created_at: row.created_at,
87
+ updated_at: row.updated_at,
88
+ locale: row.locale,
89
+ translation_group: row.translation_group
90
+ })
91
+ }));
92
+ }
93
+ /**
94
+ * Find every menu row matching `name` (one per locale on multi-locale
95
+ * installs). Callers use this both to look up a single menu (when locale
96
+ * is supplied) and to detect AMBIGUOUS_LOCALE situations (`length > 1`).
97
+ */
98
+ async findByName(name, options = {}) {
99
+ let query = this.db.selectFrom("_dineway_menus").selectAll().where("name", "=", name).orderBy("locale", "asc");
100
+ if (options.locale !== void 0) query = query.where("locale", "=", options.locale);
101
+ return (await query.execute()).map(rowToMenu);
102
+ }
103
+ async findById(id) {
104
+ const row = await this.db.selectFrom("_dineway_menus").selectAll().where("id", "=", id).executeTakeFirst();
105
+ return row ? rowToMenu(row) : null;
106
+ }
107
+ /** Fetch a menu plus its items, ordered by `sort_order`. */
108
+ async findWithItems(menuId) {
109
+ const menu = await this.findById(menuId);
110
+ if (!menu) return null;
111
+ const items = await this.findItems(menuId);
112
+ return {
113
+ ...menu,
114
+ items
115
+ };
116
+ }
117
+ async findItems(menuId) {
118
+ return (await this.db.selectFrom("_dineway_menu_items").selectAll().where("menu_id", "=", menuId).orderBy("sort_order", "asc").execute()).map(rowToMenuItem);
119
+ }
120
+ /**
121
+ * Returns true when a menu already exists for the given `(name, locale)`.
122
+ * Used by the handler to surface a CONFLICT before attempting the insert.
123
+ */
124
+ async existsByNameAndLocale(name, locale) {
125
+ return await this.db.selectFrom("_dineway_menus").select("id").where("name", "=", name).where("locale", "=", locale).executeTakeFirst() !== void 0;
126
+ }
127
+ /**
128
+ * Create a menu. When `translationOf` is supplied the new menu joins the
129
+ * source menu's translation_group and clones its items (each clone gets a
130
+ * fresh ULID, but inherits the source item's `translation_group` so a
131
+ * given nav entry resolves to "the same item" across menu translations).
132
+ *
133
+ * If the source menu is missing this throws — callers should validate
134
+ * existence via `findById` first to return a clean NOT_FOUND.
135
+ */
136
+ async create(input) {
137
+ const id = ulid();
138
+ let translationGroup = id;
139
+ let sourceMenuId = null;
140
+ if (input.translationOf) {
141
+ const source = await this.findById(input.translationOf);
142
+ if (!source) throw new Error("Source menu for translation not found");
143
+ translationGroup = source.translationGroup ?? source.id;
144
+ sourceMenuId = source.id;
145
+ }
146
+ await withTransaction(this.db, async (trx) => {
147
+ await trx.insertInto("_dineway_menus").values({
148
+ id,
149
+ name: input.name,
150
+ label: input.label,
151
+ ...input.locale !== void 0 ? { locale: input.locale } : {},
152
+ translation_group: translationGroup
153
+ }).execute();
154
+ if (sourceMenuId) {
155
+ const sourceItems = await trx.selectFrom("_dineway_menu_items").selectAll().where("menu_id", "=", sourceMenuId).orderBy("sort_order", "asc").execute();
156
+ if (sourceItems.length > 0) {
157
+ const idMap = /* @__PURE__ */ new Map();
158
+ for (const item of sourceItems) idMap.set(item.id, ulid());
159
+ await trx.insertInto("_dineway_menu_items").values(sourceItems.map((item) => ({
160
+ id: idMap.get(item.id),
161
+ menu_id: id,
162
+ parent_id: item.parent_id ? idMap.get(item.parent_id) ?? null : null,
163
+ sort_order: item.sort_order,
164
+ type: item.type,
165
+ reference_collection: item.reference_collection,
166
+ reference_id: item.reference_id,
167
+ custom_url: item.custom_url,
168
+ label: item.label,
169
+ title_attr: item.title_attr,
170
+ target: item.target,
171
+ css_classes: item.css_classes,
172
+ ...input.locale !== void 0 ? { locale: input.locale } : {},
173
+ translation_group: item.translation_group ?? item.id
174
+ }))).execute();
175
+ }
176
+ }
177
+ });
178
+ const created = await this.findById(id);
179
+ if (!created) throw new Error("Failed to create menu");
180
+ return created;
181
+ }
182
+ async update(id, input) {
183
+ if (!await this.findById(id)) return null;
184
+ const values = {};
185
+ if (input.label !== void 0) values.label = input.label;
186
+ if (Object.keys(values).length > 0) await this.db.updateTable("_dineway_menus").set(values).where("id", "=", id).execute();
187
+ return await this.findById(id);
188
+ }
189
+ /**
190
+ * Delete a menu. Items are deleted explicitly first because D1 disables
191
+ * FK enforcement at the database level, so `ON DELETE CASCADE` declared in
192
+ * migration 005 cannot be relied on. On SQLite/Postgres the cascade also
193
+ * fires, making the explicit delete a no-op there.
194
+ */
195
+ async delete(id) {
196
+ if (!await this.findById(id)) return false;
197
+ await withTransaction(this.db, async (trx) => {
198
+ await trx.deleteFrom("_dineway_menu_items").where("menu_id", "=", id).execute();
199
+ await trx.deleteFrom("_dineway_menus").where("id", "=", id).execute();
200
+ });
201
+ return true;
202
+ }
203
+ /**
204
+ * List every translation of a menu (by id or translation_group).
205
+ *
206
+ * Returns `null` when neither the id nor the group resolves to a menu,
207
+ * mapped to NOT_FOUND by the handler.
208
+ */
209
+ async listTranslations(idOrGroup) {
210
+ const anchor = await this.db.selectFrom("_dineway_menus").selectAll().where((eb) => eb.or([eb("id", "=", idOrGroup), eb("translation_group", "=", idOrGroup)])).executeTakeFirst();
211
+ if (!anchor) return null;
212
+ const group = anchor.translation_group ?? anchor.id;
213
+ return {
214
+ translationGroup: group,
215
+ translations: (await this.db.selectFrom("_dineway_menus").selectAll().where("translation_group", "=", group).orderBy("locale", "asc").execute()).map((row) => ({
216
+ id: row.id,
217
+ name: row.name,
218
+ locale: row.locale,
219
+ label: row.label,
220
+ updatedAt: row.updated_at
221
+ }))
222
+ };
223
+ }
224
+ /**
225
+ * Insert a menu item. `locale` is propagated from the parent menu so
226
+ * `_dineway_menu_items.locale` mirrors the menu's locale (queries can scope
227
+ * by locale without a join).
228
+ *
229
+ * When `sortOrder` is omitted, the next position within the same parent
230
+ * scope is used (max + 1). The fresh `translation_group` defaults to the
231
+ * item's own id, matching the migration 036 backfill.
232
+ */
233
+ async createItem(menuId, locale, input) {
234
+ let sortOrder = input.sortOrder ?? 0;
235
+ if (input.sortOrder === void 0) sortOrder = ((await this.db.selectFrom("_dineway_menu_items").select(({ fn }) => fn.max("sort_order").as("max")).where("menu_id", "=", menuId).where("parent_id", "is", input.parentId ?? null).executeTakeFirst())?.max ?? -1) + 1;
236
+ const id = ulid();
237
+ await this.db.insertInto("_dineway_menu_items").values({
238
+ id,
239
+ menu_id: menuId,
240
+ parent_id: input.parentId ?? null,
241
+ sort_order: sortOrder,
242
+ type: input.type,
243
+ reference_collection: input.referenceCollection ?? null,
244
+ reference_id: input.referenceId ?? null,
245
+ custom_url: input.customUrl ?? null,
246
+ label: input.label,
247
+ title_attr: input.titleAttr ?? null,
248
+ target: input.target ?? null,
249
+ css_classes: input.cssClasses ?? null,
250
+ locale,
251
+ translation_group: id
252
+ }).execute();
253
+ return rowToMenuItem(await this.db.selectFrom("_dineway_menu_items").selectAll().where("id", "=", id).executeTakeFirstOrThrow());
254
+ }
255
+ /**
256
+ * Update a menu item. Caller must ensure the item belongs to the menu —
257
+ * the `where("menu_id", "=", menuId)` guard prevents cross-menu writes.
258
+ * Returns `null` if the item is not found within the menu.
259
+ */
260
+ async updateItem(menuId, itemId, input) {
261
+ if (!await this.db.selectFrom("_dineway_menu_items").select("id").where("id", "=", itemId).where("menu_id", "=", menuId).executeTakeFirst()) return null;
262
+ const values = {};
263
+ if (input.label !== void 0) values.label = input.label;
264
+ if (input.customUrl !== void 0) values.custom_url = input.customUrl;
265
+ if (input.target !== void 0) values.target = input.target;
266
+ if (input.titleAttr !== void 0) values.title_attr = input.titleAttr;
267
+ if (input.cssClasses !== void 0) values.css_classes = input.cssClasses;
268
+ if (input.parentId !== void 0) values.parent_id = input.parentId;
269
+ if (input.sortOrder !== void 0) values.sort_order = input.sortOrder;
270
+ if (Object.keys(values).length > 0) await this.db.updateTable("_dineway_menu_items").set(values).where("id", "=", itemId).execute();
271
+ return rowToMenuItem(await this.db.selectFrom("_dineway_menu_items").selectAll().where("id", "=", itemId).executeTakeFirstOrThrow());
272
+ }
273
+ /** Delete an item scoped to its menu. Returns false if nothing was deleted. */
274
+ async deleteItem(menuId, itemId) {
275
+ return (await this.db.deleteFrom("_dineway_menu_items").where("id", "=", itemId).where("menu_id", "=", menuId).execute())[0]?.numDeletedRows !== 0n;
276
+ }
277
+ /**
278
+ * Atomic replace: delete every existing item and re-insert in order.
279
+ * `parentIndex` (validated by the caller) is resolved against the live
280
+ * insert order so children always reference real parent ids.
281
+ *
282
+ * Returns the count of inserted items (matches the existing handler API).
283
+ */
284
+ async setItems(menuId, locale, items) {
285
+ await withTransaction(this.db, async (trx) => {
286
+ if (!await trx.selectFrom("_dineway_menus").select("id").where("id", "=", menuId).executeTakeFirst()) throw new MenuGoneError(menuId);
287
+ await trx.deleteFrom("_dineway_menu_items").where("menu_id", "=", menuId).execute();
288
+ const insertedIds = [];
289
+ for (let i = 0; i < items.length; i++) {
290
+ const item = items[i];
291
+ if (!item) continue;
292
+ const id = ulid();
293
+ const parentId = item.parentIndex !== void 0 ? insertedIds[item.parentIndex] ?? null : null;
294
+ await trx.insertInto("_dineway_menu_items").values({
295
+ id,
296
+ menu_id: menuId,
297
+ parent_id: parentId,
298
+ sort_order: i,
299
+ type: item.type,
300
+ reference_collection: item.referenceCollection ?? null,
301
+ reference_id: item.referenceId ?? null,
302
+ custom_url: item.customUrl ?? null,
303
+ label: item.label,
304
+ title_attr: item.titleAttr ?? null,
305
+ target: item.target ?? null,
306
+ css_classes: item.cssClasses ?? null,
307
+ locale
308
+ }).execute();
309
+ insertedIds.push(id);
310
+ }
311
+ await trx.updateTable("_dineway_menus").set({ updated_at: (/* @__PURE__ */ new Date()).toISOString() }).where("id", "=", menuId).execute();
312
+ });
313
+ return { itemCount: items.length };
314
+ }
315
+ /**
316
+ * Batch reorder items. Each entry is applied scoped to the menu so a
317
+ * malicious payload cannot move foreign items into this menu's siblings.
318
+ */
319
+ async reorderItems(menuId, items) {
320
+ return withTransaction(this.db, async (trx) => {
321
+ for (const item of items) await trx.updateTable("_dineway_menu_items").set({
322
+ parent_id: item.parentId,
323
+ sort_order: item.sortOrder
324
+ }).where("id", "=", item.id).where("menu_id", "=", menuId).execute();
325
+ return (await trx.selectFrom("_dineway_menu_items").selectAll().where("menu_id", "=", menuId).orderBy("sort_order", "asc").execute()).map(rowToMenuItem);
326
+ });
327
+ }
328
+ };
329
+
330
+ //#endregion
331
+ //#region src/api/handlers/menus.ts
332
+ var menus_exports = /* @__PURE__ */ __exportAll({
333
+ handleMenuCreate: () => handleMenuCreate,
334
+ handleMenuDelete: () => handleMenuDelete,
335
+ handleMenuGet: () => handleMenuGet,
336
+ handleMenuItemCreate: () => handleMenuItemCreate,
337
+ handleMenuItemDelete: () => handleMenuItemDelete,
338
+ handleMenuItemReorder: () => handleMenuItemReorder,
339
+ handleMenuItemUpdate: () => handleMenuItemUpdate,
340
+ handleMenuList: () => handleMenuList,
341
+ handleMenuSetItems: () => handleMenuSetItems,
342
+ handleMenuTranslations: () => handleMenuTranslations,
343
+ handleMenuUpdate: () => handleMenuUpdate
344
+ });
345
+ /**
346
+ * Error returned when a menu lookup by `name` matches multiple locale
347
+ * variants and the caller did not pass `locale` to disambiguate. Maps to
348
+ * HTTP 400 via `mapErrorStatus`. The available locales are surfaced in the
349
+ * message so MCP/REST callers can recover by re-issuing with `locale`.
350
+ */
351
+ function ambiguousMenuLocaleError(name, locales) {
352
+ return {
353
+ success: false,
354
+ error: {
355
+ code: "AMBIGUOUS_LOCALE",
356
+ message: `Menu '${name}' exists in multiple locales (${locales.toSorted().join(", ")}); pass 'locale' to disambiguate.`
357
+ }
358
+ };
359
+ }
360
+ /**
361
+ * Resolve a menu by name + optional locale to a single Menu, surfacing the
362
+ * canonical NOT_FOUND / AMBIGUOUS_LOCALE errors. Every item handler relies on
363
+ * this to translate (name, locale) into an unambiguous menu row.
364
+ */
365
+ async function resolveMenu(repo, name, options) {
366
+ const matches = await repo.findByName(name, options);
367
+ if (matches.length === 0) return {
368
+ success: false,
369
+ error: {
370
+ code: "NOT_FOUND",
371
+ message: `Menu '${name}' not found${options.locale ? ` in locale '${options.locale}'` : ""}`
372
+ }
373
+ };
374
+ if (matches.length > 1) return {
375
+ success: false,
376
+ error: ambiguousMenuLocaleError(name, matches.map((m) => m.locale)).error
377
+ };
378
+ return {
379
+ success: true,
380
+ menu: matches[0]
381
+ };
382
+ }
383
+ /**
384
+ * List menus with item counts. Filter by `locale` when provided.
385
+ */
386
+ async function handleMenuList(db, options = {}) {
387
+ try {
388
+ return {
389
+ success: true,
390
+ data: await new MenuRepository(db).findMany(options)
391
+ };
392
+ } catch {
393
+ return {
394
+ success: false,
395
+ error: {
396
+ code: "MENU_LIST_ERROR",
397
+ message: "Failed to fetch menus"
398
+ }
399
+ };
400
+ }
401
+ }
402
+ /**
403
+ * Create a new menu. When `translationOf` is supplied the new menu joins the
404
+ * source menu's translation_group (and gets the source's items cloned by the
405
+ * repository).
406
+ */
407
+ async function handleMenuCreate(db, input) {
408
+ try {
409
+ if (input.translationOf && !input.locale) return {
410
+ success: false,
411
+ error: {
412
+ code: "VALIDATION_ERROR",
413
+ message: "`locale` is required when `translationOf` is provided"
414
+ }
415
+ };
416
+ const repo = new MenuRepository(db);
417
+ if (input.translationOf) {
418
+ if (!await repo.findById(input.translationOf)) return {
419
+ success: false,
420
+ error: {
421
+ code: "NOT_FOUND",
422
+ message: "Source menu for translation not found"
423
+ }
424
+ };
425
+ }
426
+ const effectiveLocale = input.locale ?? getI18nConfig()?.defaultLocale ?? "en";
427
+ if (await repo.existsByNameAndLocale(input.name, effectiveLocale)) return {
428
+ success: false,
429
+ error: {
430
+ code: "CONFLICT",
431
+ message: `Menu "${input.name}" already exists${input.locale ? ` in locale "${input.locale}"` : ""}`
432
+ }
433
+ };
434
+ return {
435
+ success: true,
436
+ data: await repo.create(input)
437
+ };
438
+ } catch {
439
+ return {
440
+ success: false,
441
+ error: {
442
+ code: "MENU_CREATE_ERROR",
443
+ message: "Failed to create menu"
444
+ }
445
+ };
446
+ }
447
+ }
448
+ /**
449
+ * Get a single menu by name. Honours an optional `locale` filter; when two
450
+ * menus share a name across locales, the locale distinguishes them.
451
+ *
452
+ * Historical behaviour: when `locale` is omitted, returns the lowest-locale
453
+ * match (deterministic). Mirrors the pre-repo handler.
454
+ */
455
+ async function handleMenuGet(db, name, options = {}) {
456
+ try {
457
+ const repo = new MenuRepository(db);
458
+ const matches = await repo.findByName(name, options);
459
+ if (matches.length === 0) return {
460
+ success: false,
461
+ error: {
462
+ code: "NOT_FOUND",
463
+ message: `Menu '${name}' not found`
464
+ }
465
+ };
466
+ const menu = matches[0];
467
+ const items = await repo.findItems(menu.id);
468
+ return {
469
+ success: true,
470
+ data: {
471
+ ...menu,
472
+ items
473
+ }
474
+ };
475
+ } catch {
476
+ return {
477
+ success: false,
478
+ error: {
479
+ code: "MENU_GET_ERROR",
480
+ message: "Failed to fetch menu"
481
+ }
482
+ };
483
+ }
484
+ }
485
+ /**
486
+ * Update a menu's label. The name + locale are immutable.
487
+ */
488
+ async function handleMenuUpdate(db, name, input) {
489
+ try {
490
+ const repo = new MenuRepository(db);
491
+ const resolved = await resolveMenu(repo, name, { locale: input.locale });
492
+ if (!resolved.success) return resolved;
493
+ const updated = await repo.update(resolved.menu.id, { label: input.label });
494
+ if (!updated) return {
495
+ success: false,
496
+ error: {
497
+ code: "NOT_FOUND",
498
+ message: `Menu '${name}' not found`
499
+ }
500
+ };
501
+ return {
502
+ success: true,
503
+ data: updated
504
+ };
505
+ } catch {
506
+ return {
507
+ success: false,
508
+ error: {
509
+ code: "MENU_UPDATE_ERROR",
510
+ message: "Failed to update menu"
511
+ }
512
+ };
513
+ }
514
+ }
515
+ /**
516
+ * Delete a menu (and its items, via the repository's explicit cleanup).
517
+ */
518
+ async function handleMenuDelete(db, name, options = {}) {
519
+ try {
520
+ const repo = new MenuRepository(db);
521
+ const resolved = await resolveMenu(repo, name, options);
522
+ if (!resolved.success) return resolved;
523
+ await repo.delete(resolved.menu.id);
524
+ return {
525
+ success: true,
526
+ data: { deleted: true }
527
+ };
528
+ } catch {
529
+ return {
530
+ success: false,
531
+ error: {
532
+ code: "MENU_DELETE_ERROR",
533
+ message: "Failed to delete menu"
534
+ }
535
+ };
536
+ }
537
+ }
538
+ /**
539
+ * List every translation of a menu (by id or translation_group).
540
+ */
541
+ async function handleMenuTranslations(db, idOrGroup) {
542
+ try {
543
+ const result = await new MenuRepository(db).listTranslations(idOrGroup);
544
+ if (!result) return {
545
+ success: false,
546
+ error: {
547
+ code: "NOT_FOUND",
548
+ message: "Menu not found"
549
+ }
550
+ };
551
+ return {
552
+ success: true,
553
+ data: result
554
+ };
555
+ } catch {
556
+ return {
557
+ success: false,
558
+ error: {
559
+ code: "MENU_TRANSLATIONS_ERROR",
560
+ message: "Failed to list menu translations"
561
+ }
562
+ };
563
+ }
564
+ }
565
+ /**
566
+ * Add an item to a menu. The item inherits the menu's locale.
567
+ */
568
+ async function handleMenuItemCreate(db, menuName, input, options = {}) {
569
+ try {
570
+ const repo = new MenuRepository(db);
571
+ const resolved = await resolveMenu(repo, menuName, options);
572
+ if (!resolved.success) return resolved;
573
+ return {
574
+ success: true,
575
+ data: await repo.createItem(resolved.menu.id, resolved.menu.locale, input)
576
+ };
577
+ } catch {
578
+ return {
579
+ success: false,
580
+ error: {
581
+ code: "MENU_ITEM_CREATE_ERROR",
582
+ message: "Failed to create menu item"
583
+ }
584
+ };
585
+ }
586
+ }
587
+ /**
588
+ * Update a menu item.
589
+ */
590
+ async function handleMenuItemUpdate(db, menuName, itemId, input, options = {}) {
591
+ try {
592
+ const repo = new MenuRepository(db);
593
+ const resolved = await resolveMenu(repo, menuName, options);
594
+ if (!resolved.success) return resolved;
595
+ const updated = await repo.updateItem(resolved.menu.id, itemId, input);
596
+ if (!updated) return {
597
+ success: false,
598
+ error: {
599
+ code: "NOT_FOUND",
600
+ message: "Menu item not found"
601
+ }
602
+ };
603
+ return {
604
+ success: true,
605
+ data: updated
606
+ };
607
+ } catch {
608
+ return {
609
+ success: false,
610
+ error: {
611
+ code: "MENU_ITEM_UPDATE_ERROR",
612
+ message: "Failed to update menu item"
613
+ }
614
+ };
615
+ }
616
+ }
617
+ /**
618
+ * Delete a menu item.
619
+ */
620
+ async function handleMenuItemDelete(db, menuName, itemId, options = {}) {
621
+ try {
622
+ const repo = new MenuRepository(db);
623
+ const resolved = await resolveMenu(repo, menuName, options);
624
+ if (!resolved.success) return resolved;
625
+ if (!await repo.deleteItem(resolved.menu.id, itemId)) return {
626
+ success: false,
627
+ error: {
628
+ code: "NOT_FOUND",
629
+ message: "Menu item not found"
630
+ }
631
+ };
632
+ return {
633
+ success: true,
634
+ data: { deleted: true }
635
+ };
636
+ } catch {
637
+ return {
638
+ success: false,
639
+ error: {
640
+ code: "MENU_ITEM_DELETE_ERROR",
641
+ message: "Failed to delete menu item"
642
+ }
643
+ };
644
+ }
645
+ }
646
+ /**
647
+ * Replace the entire set of items for a menu in one atomic transaction.
648
+ *
649
+ * Existing items are deleted and the new list is inserted in the order
650
+ * provided. `parentIndex` references resolve to actual parent IDs as the
651
+ * insert proceeds.
652
+ */
653
+ async function handleMenuSetItems(db, menuName, items, options = {}) {
654
+ for (let i = 0; i < items.length; i++) {
655
+ const item = items[i];
656
+ if (item?.parentIndex !== void 0) {
657
+ if (item.parentIndex < 0 || item.parentIndex >= i) return {
658
+ success: false,
659
+ error: {
660
+ code: "VALIDATION_ERROR",
661
+ message: `item[${i}].parentIndex (${item.parentIndex}) must reference an earlier item`
662
+ }
663
+ };
664
+ }
665
+ }
666
+ try {
667
+ const repo = new MenuRepository(db);
668
+ const resolved = await resolveMenu(repo, menuName, options);
669
+ if (!resolved.success) return resolved;
670
+ const { itemCount } = await repo.setItems(resolved.menu.id, resolved.menu.locale, items);
671
+ return {
672
+ success: true,
673
+ data: {
674
+ name: menuName,
675
+ itemCount
676
+ }
677
+ };
678
+ } catch (error) {
679
+ if (error instanceof MenuGoneError) return {
680
+ success: false,
681
+ error: {
682
+ code: "NOT_FOUND",
683
+ message: `Menu '${menuName}' not found${options.locale ? ` in locale '${options.locale}'` : ""}`
684
+ }
685
+ };
686
+ console.error("[dineway] handleMenuSetItems failed:", error);
687
+ return {
688
+ success: false,
689
+ error: {
690
+ code: "MENU_SET_ITEMS_ERROR",
691
+ message: "Failed to set menu items"
692
+ }
693
+ };
694
+ }
695
+ }
696
+ /**
697
+ * Batch reorder menu items.
698
+ */
699
+ async function handleMenuItemReorder(db, menuName, items, options = {}) {
700
+ try {
701
+ const repo = new MenuRepository(db);
702
+ const resolved = await resolveMenu(repo, menuName, options);
703
+ if (!resolved.success) return resolved;
704
+ return {
705
+ success: true,
706
+ data: await repo.reorderItems(resolved.menu.id, items)
707
+ };
708
+ } catch {
709
+ return {
710
+ success: false,
711
+ error: {
712
+ code: "MENU_REORDER_ERROR",
713
+ message: "Failed to reorder menu items"
714
+ }
715
+ };
716
+ }
717
+ }
718
+
719
+ //#endregion
720
+ export { handleMenuItemDelete as a, handleMenuList as c, menus_exports as d, handleMenuItemCreate as i, handleMenuTranslations as l, handleMenuDelete as n, handleMenuItemReorder as o, handleMenuGet as r, handleMenuItemUpdate as s, handleMenuCreate as t, handleMenuUpdate as u };