dineway 0.1.4 → 0.1.5

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 (193) hide show
  1. package/README.md +6 -3
  2. package/dist/{apply-CAPvMfoU.mjs → apply-iVSqz2qs.mjs} +132 -39
  3. package/dist/astro/index.d.mts +18 -9
  4. package/dist/astro/index.mjs +238 -16
  5. package/dist/astro/middleware/auth.d.mts +16 -5
  6. package/dist/astro/middleware/auth.mjs +74 -37
  7. package/dist/astro/middleware/redirect.mjs +24 -8
  8. package/dist/astro/middleware/request-context.mjs +18 -5
  9. package/dist/astro/middleware/setup.mjs +1 -1
  10. package/dist/astro/middleware.mjs +411 -169
  11. package/dist/astro/types.d.mts +25 -8
  12. package/dist/{byline-DeWCMU_i.mjs → byline-OhH2dlRu.mjs} +6 -21
  13. package/dist/{bylines-DyqBV9EQ.mjs → bylines-BGpD9_hy.mjs} +16 -6
  14. package/dist/cache-BdSY-gQN.mjs +42 -0
  15. package/dist/chunks--4F8ddV4.mjs +18 -0
  16. package/dist/cli/index.mjs +935 -15
  17. package/dist/client/external-auth-headers.d.mts +1 -1
  18. package/dist/client/index.d.mts +11 -3
  19. package/dist/client/index.mjs +4 -3
  20. package/dist/{connection-C9pxzuag.mjs → connection-BCNICDWN.mjs} +22 -5
  21. package/dist/{content-zSgdNmnt.mjs → content-DWi4d0rT.mjs} +41 -2
  22. package/dist/database/instrumentation.d.mts +34 -0
  23. package/dist/database/instrumentation.mjs +53 -0
  24. package/dist/db/index.d.mts +3 -3
  25. package/dist/db/index.mjs +2 -2
  26. package/dist/db/libsql.d.mts +1 -1
  27. package/dist/db/libsql.mjs +11 -5
  28. package/dist/db/postgres.d.mts +1 -1
  29. package/dist/db/sqlite.d.mts +1 -1
  30. package/dist/db/sqlite.mjs +7 -1
  31. package/dist/db-errors-CEqD7qH9.mjs +23 -0
  32. package/dist/{default-WYlzADZL.mjs → default-VjJyuuG9.mjs} +2 -0
  33. package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +3 -0
  34. package/dist/{error-DrxtnGPg.mjs → error-BmL6QipT.mjs} +7 -3
  35. package/dist/{index-C-jx21qs.d.mts → index-yvc6E_17.d.mts} +157 -30
  36. package/dist/index.d.mts +11 -11
  37. package/dist/index.mjs +24 -22
  38. package/dist/{loader-qKmo0wAY.mjs → loader-sMG4TZ-u.mjs} +9 -3
  39. package/dist/media/index.d.mts +1 -1
  40. package/dist/media/index.mjs +1 -1
  41. package/dist/media/local-runtime.d.mts +7 -7
  42. package/dist/page/index.d.mts +10 -2
  43. package/dist/page/index.mjs +22 -1
  44. package/dist/patterns-CrCYkMBb.mjs +92 -0
  45. package/dist/{placeholder-bOx1xCTY.d.mts → placeholder--wOi4TbO.d.mts} +1 -1
  46. package/dist/{placeholder-B3knXwNc.mjs → placeholder-Cp8g5Emj.mjs} +1 -1
  47. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  48. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  49. package/dist/{query-BiaPl_g2.mjs → query-kDmwCsHh.mjs} +118 -50
  50. package/dist/{redirect-JPqLAbxa.mjs → redirect-DnEWAkVg.mjs} +43 -99
  51. package/dist/{registry-DSd1GWB8.mjs → registry-C0zjeB9P.mjs} +191 -123
  52. package/dist/request-cache-Dk5qPSOx.mjs +66 -0
  53. package/dist/request-context.d.mts +4 -16
  54. package/dist/{runner-B5l1JfOj.d.mts → runner-CFI6B6J2.d.mts} +1 -1
  55. package/dist/{runner-BGUGywgG.mjs → runner-DWZm2KQm.mjs} +589 -137
  56. package/dist/runtime.d.mts +6 -6
  57. package/dist/runtime.mjs +2 -2
  58. package/dist/{search-BNruJHDL.mjs → search-ByRGV2pq.mjs} +570 -424
  59. package/dist/seed/index.d.mts +2 -2
  60. package/dist/seed/index.mjs +11 -10
  61. package/dist/seo/index.d.mts +1 -1
  62. package/dist/storage/local.d.mts +1 -1
  63. package/dist/storage/local.mjs +1 -1
  64. package/dist/storage/s3.d.mts +11 -3
  65. package/dist/storage/s3.mjs +78 -15
  66. package/dist/taxonomies-1s5PaS_8.mjs +266 -0
  67. package/dist/transaction-Cn2rjY78.mjs +27 -0
  68. package/dist/{types-BgQeVaPj.d.mts → types-BuMDPy5C.d.mts} +52 -3
  69. package/dist/{types-DuNbGKjF.mjs → types-COeOq9nK.mjs} +6 -1
  70. package/dist/{types-ju-_ORz7.d.mts → types-CWbdtiux.d.mts} +13 -5
  71. package/dist/{types-D38djUXv.d.mts → types-Cj0KMIZV.d.mts} +16 -3
  72. package/dist/{types-DkvMXalq.d.mts → types-DOrVigru.d.mts} +159 -0
  73. package/dist/{validate-CXnRKfJK.mjs → validate-BZ5wnLLp.mjs} +2 -1
  74. package/dist/{validate-DVKJJ-M_.d.mts → validate-IPf8n4Fj.d.mts} +4 -51
  75. package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +10 -10
  76. package/dist/version-BKXPsfmJ.mjs +6 -0
  77. package/package.json +49 -38
  78. package/src/astro/routes/admin.astro +25 -9
  79. package/src/astro/routes/api/admin/api-tokens/[id].ts +4 -0
  80. package/src/astro/routes/api/admin/api-tokens/index.ts +24 -2
  81. package/src/astro/routes/api/admin/briefing.ts +76 -0
  82. package/src/astro/routes/api/admin/bylines/[id]/index.ts +3 -0
  83. package/src/astro/routes/api/admin/bylines/index.ts +2 -0
  84. package/src/astro/routes/api/admin/context/[id]/history.ts +35 -0
  85. package/src/astro/routes/api/admin/context/[id]/index.ts +35 -0
  86. package/src/astro/routes/api/admin/context/[id]/review.ts +57 -0
  87. package/src/astro/routes/api/admin/context/[id]/supersede.ts +58 -0
  88. package/src/astro/routes/api/admin/context/diff.ts +35 -0
  89. package/src/astro/routes/api/admin/context/index.ts +69 -0
  90. package/src/astro/routes/api/admin/context/stale.ts +35 -0
  91. package/src/astro/routes/api/admin/hitl-requests/[id]/index.ts +38 -0
  92. package/src/astro/routes/api/admin/hitl-requests/[id]/resolve.ts +54 -0
  93. package/src/astro/routes/api/admin/hitl-requests/index.ts +38 -0
  94. package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +58 -17
  95. package/src/astro/routes/api/admin/oauth-clients/[id].ts +28 -1
  96. package/src/astro/routes/api/admin/oauth-clients/index.ts +25 -1
  97. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +54 -2
  98. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +54 -2
  99. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +51 -1
  100. package/src/astro/routes/api/admin/plugins/[id]/update.ts +98 -3
  101. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +72 -1
  102. package/src/astro/routes/api/admin/review-requests/[id]/index.ts +35 -0
  103. package/src/astro/routes/api/admin/review-requests/[id]/resolve.ts +52 -0
  104. package/src/astro/routes/api/admin/review-requests/index.ts +35 -0
  105. package/src/astro/routes/api/admin/users/[id]/disable.ts +26 -23
  106. package/src/astro/routes/api/admin/users/[id]/index.ts +41 -21
  107. package/src/astro/routes/api/auth/invite/register-options.ts +73 -0
  108. package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
  109. package/src/astro/routes/api/auth/passkey/options.ts +2 -1
  110. package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
  111. package/src/astro/routes/api/auth/signup/request.ts +20 -8
  112. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +3 -4
  113. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
  114. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +16 -2
  115. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +16 -0
  116. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +9 -0
  117. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
  118. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +45 -1
  119. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +12 -2
  120. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
  121. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +24 -0
  122. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +3 -0
  123. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +20 -0
  124. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +13 -0
  125. package/src/astro/routes/api/content/[collection]/[id].ts +36 -0
  126. package/src/astro/routes/api/content/[collection]/index.ts +48 -4
  127. package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
  128. package/src/astro/routes/api/health.ts +54 -0
  129. package/src/astro/routes/api/import/wordpress/analyze.ts +2 -10
  130. package/src/astro/routes/api/import/wordpress/execute.ts +40 -6
  131. package/src/astro/routes/api/import/wordpress/prepare.ts +36 -5
  132. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +33 -1
  133. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +3 -3
  134. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +57 -15
  135. package/src/astro/routes/api/manifest.ts +13 -1
  136. package/src/astro/routes/api/mcp.ts +1 -0
  137. package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +7 -2
  138. package/src/astro/routes/api/media/upload-url.ts +11 -2
  139. package/src/astro/routes/api/media.ts +9 -7
  140. package/src/astro/routes/api/menus/[name]/items.ts +124 -5
  141. package/src/astro/routes/api/menus/[name]/reorder.ts +47 -1
  142. package/src/astro/routes/api/menus/[name].ts +84 -4
  143. package/src/astro/routes/api/menus/index.ts +46 -2
  144. package/src/astro/routes/api/oauth/authorize.ts +21 -8
  145. package/src/astro/routes/api/oauth/device/code.ts +2 -1
  146. package/src/astro/routes/api/oauth/device/token.ts +2 -1
  147. package/src/astro/routes/api/oauth/register.ts +182 -0
  148. package/src/astro/routes/api/oauth/token.ts +18 -7
  149. package/src/astro/routes/api/openapi.json.ts +3 -2
  150. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +21 -4
  151. package/src/astro/routes/api/redirects/[id].ts +103 -4
  152. package/src/astro/routes/api/redirects/index.ts +50 -2
  153. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +28 -0
  154. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +15 -0
  155. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +13 -0
  156. package/src/astro/routes/api/schema/collections/[slug]/index.ts +27 -0
  157. package/src/astro/routes/api/schema/collections/index.ts +14 -0
  158. package/src/astro/routes/api/search/index.ts +1 -0
  159. package/src/astro/routes/api/search/suggest.ts +1 -0
  160. package/src/astro/routes/api/sections/[slug].ts +123 -4
  161. package/src/astro/routes/api/sections/index.ts +57 -2
  162. package/src/astro/routes/api/settings.ts +51 -2
  163. package/src/astro/routes/api/setup/admin-verify.ts +25 -5
  164. package/src/astro/routes/api/setup/admin.ts +16 -8
  165. package/src/astro/routes/api/setup/index.ts +3 -2
  166. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +141 -4
  167. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +64 -2
  168. package/src/astro/routes/api/taxonomies/index.ts +57 -2
  169. package/src/astro/routes/api/well-known/auth.ts +3 -1
  170. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +8 -5
  171. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +3 -2
  172. package/src/astro/routes/api/widget-areas/[name]/reorder.ts +58 -16
  173. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +124 -38
  174. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +66 -20
  175. package/src/astro/routes/api/widget-areas/[name].ts +55 -7
  176. package/src/astro/routes/api/widget-areas/index.ts +56 -6
  177. package/src/components/DinewayHead.astro +15 -7
  178. package/src/components/DinewayMedia.astro +1 -1
  179. package/src/components/InlinePortableTextEditor.tsx +1 -1
  180. package/src/components/Table.astro +68 -41
  181. package/src/components/index.ts +2 -12
  182. package/src/components/marks.ts +19 -0
  183. package/LICENSE +0 -9
  184. /package/dist/{adapters-BlzWJG82.d.mts → adapters-C2ypTrZZ.d.mts} +0 -0
  185. /package/dist/{config-Cq8H0SfX.mjs → config-BXwuX8Bx.mjs} +0 -0
  186. /package/dist/{load-C6FCD1FU.mjs → load-Coc9HpHH.mjs} +0 -0
  187. /package/dist/{manifest-schema-CTSEyIJ3.mjs → manifest-schema-D1MSVnoI.mjs} +0 -0
  188. /package/dist/{mode-BlyYtIFO.mjs → mode-47goXBBK.mjs} +0 -0
  189. /package/dist/{tokens-4vgYuXsZ.mjs → tokens-CJz9ubV6.mjs} +0 -0
  190. /package/dist/{transport-C5FYnid7.mjs → transport-DB5eDN4x.mjs} +0 -0
  191. /package/dist/{transport-gIL-e43D.d.mts → transport-Wge_IzKl.d.mts} +0 -0
  192. /package/dist/{types-CLLdsG3g.d.mts → types-BzcUjoqg.d.mts} +0 -0
  193. /package/dist/{types-DShnjzb6.mjs → types-griIBQOQ.mjs} +0 -0
@@ -1,36 +1,11 @@
1
1
  import { t as __exportAll } from "./chunk-ClPoSABd.mjs";
2
- import { a as isSqlite, c as tableExists, n as currentTimestamp, s as listTablesLike } from "./dialect-helpers-B9uSp2GJ.mjs";
3
- import { t as validateIdentifier } from "./validate-CqRJb_xU.mjs";
4
- import { i as RESERVED_FIELD_SLUGS, n as FIELD_TYPE_TO_COLUMN, r as RESERVED_COLLECTION_SLUGS } from "./types-DuNbGKjF.mjs";
2
+ import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
3
+ import { a as isSqlite, c as tableExists, n as currentTimestamp, s as listTablesLike } from "./dialect-helpers-DhTzaUxP.mjs";
4
+ import { t as withTransaction } from "./transaction-Cn2rjY78.mjs";
5
+ import { i as RESERVED_FIELD_SLUGS, n as FIELD_TYPE_TO_COLUMN, r as RESERVED_COLLECTION_SLUGS } from "./types-COeOq9nK.mjs";
5
6
  import { sql } from "kysely";
6
7
  import { ulid } from "ulidx";
7
8
 
8
- //#region src/database/transaction.ts
9
- /**
10
- * Run a callback inside a transaction if supported, or directly if not.
11
- *
12
- * Probes the database once on first call to determine if transactions work.
13
- * The result is cached for the lifetime of the process/worker.
14
- */
15
- let transactionsSupported = null;
16
- const TRANSACTIONS_NOT_SUPPORTED_RE = /transactions are not supported/i;
17
- async function withTransaction(db, fn) {
18
- if (transactionsSupported === true) return db.transaction().execute(fn);
19
- if (transactionsSupported === false) return fn(db);
20
- try {
21
- const result = await db.transaction().execute(fn);
22
- transactionsSupported = true;
23
- return result;
24
- } catch (error) {
25
- if (error instanceof Error && TRANSACTIONS_NOT_SUPPORTED_RE.test(error.message)) {
26
- transactionsSupported = false;
27
- return fn(db);
28
- }
29
- throw error;
30
- }
31
- }
32
-
33
- //#endregion
34
9
  //#region src/search/fts-manager.ts
35
10
  /**
36
11
  * FTS5 Manager
@@ -55,12 +30,14 @@ var FTSManager = class {
55
30
  * Uses _dineway_ prefix to clearly mark as internal/system table
56
31
  */
57
32
  getFtsTableName(collectionSlug) {
33
+ validateIdentifier(collectionSlug, "collection slug");
58
34
  return `_dineway_fts_${collectionSlug}`;
59
35
  }
60
36
  /**
61
37
  * Get the content table name for a collection
62
38
  */
63
39
  getContentTableName(collectionSlug) {
40
+ validateIdentifier(collectionSlug, "collection slug");
64
41
  return `ec_${collectionSlug}`;
65
42
  }
66
43
  /**
@@ -99,9 +76,14 @@ var FTSManager = class {
99
76
  await this.createTriggers(collectionSlug, searchableFields);
100
77
  }
101
78
  /**
102
- * Create triggers to keep FTS table in sync with content table
79
+ * Create triggers to keep FTS table in sync with content table.
80
+ *
81
+ * Insert and update triggers only add rows to the FTS index when
82
+ * `deleted_at IS NULL`. This keeps soft-deleted content out of the
83
+ * index and keeps repair counts aligned with searchable content.
103
84
  */
104
85
  async createTriggers(collectionSlug, searchableFields) {
86
+ this.validateInputs(collectionSlug, searchableFields);
105
87
  const ftsTable = this.getFtsTableName(collectionSlug);
106
88
  const contentTable = this.getContentTableName(collectionSlug);
107
89
  const fieldList = searchableFields.join(", ");
@@ -109,6 +91,7 @@ var FTSManager = class {
109
91
  await sql.raw(`
110
92
  CREATE TRIGGER IF NOT EXISTS "${ftsTable}_insert"
111
93
  AFTER INSERT ON "${contentTable}"
94
+ WHEN NEW.deleted_at IS NULL
112
95
  BEGIN
113
96
  INSERT INTO "${ftsTable}"(rowid, id, locale, ${fieldList})
114
97
  VALUES (NEW.rowid, NEW.id, NEW.locale, ${newFieldList});
@@ -120,7 +103,8 @@ var FTSManager = class {
120
103
  BEGIN
121
104
  DELETE FROM "${ftsTable}" WHERE rowid = OLD.rowid;
122
105
  INSERT INTO "${ftsTable}"(rowid, id, locale, ${fieldList})
123
- VALUES (NEW.rowid, NEW.id, NEW.locale, ${newFieldList});
106
+ SELECT NEW.rowid, NEW.id, NEW.locale, ${newFieldList}
107
+ WHERE NEW.deleted_at IS NULL;
124
108
  END
125
109
  `).execute(this.db);
126
110
  await sql.raw(`
@@ -135,6 +119,7 @@ var FTSManager = class {
135
119
  * Drop triggers for a collection
136
120
  */
137
121
  async dropTriggers(collectionSlug) {
122
+ this.validateInputs(collectionSlug);
138
123
  const ftsTable = this.getFtsTableName(collectionSlug);
139
124
  await sql.raw(`DROP TRIGGER IF EXISTS "${ftsTable}_insert"`).execute(this.db);
140
125
  await sql.raw(`DROP TRIGGER IF EXISTS "${ftsTable}_update"`).execute(this.db);
@@ -211,16 +196,16 @@ var FTSManager = class {
211
196
  return (await this.db.selectFrom("_dineway_fields").select("slug").where("collection_id", "=", collection.id).where("searchable", "=", 1).execute()).map((f) => f.slug);
212
197
  }
213
198
  /**
214
- * Enable search for a collection
199
+ * Enable search for a collection.
215
200
  *
216
- * Creates the FTS table and triggers, and populates from existing content.
201
+ * Rebuilds from scratch to ensure stale tables or triggers do not survive
202
+ * repeated enablement.
217
203
  */
218
204
  async enableSearch(collectionSlug, options) {
219
205
  if (!isSqlite(this.db)) throw new Error("Full-text search is only available with SQLite databases");
220
206
  const searchableFields = await this.getSearchableFields(collectionSlug);
221
207
  if (searchableFields.length === 0) throw new Error(`No searchable fields defined for collection "${collectionSlug}". Mark at least one field as searchable before enabling search.`);
222
- await this.createFtsTable(collectionSlug, searchableFields, options?.weights);
223
- await this.populateFromContent(collectionSlug, searchableFields);
208
+ await this.rebuildIndex(collectionSlug, searchableFields, options?.weights);
224
209
  await this.setSearchConfig(collectionSlug, {
225
210
  enabled: true,
226
211
  weights: options?.weights
@@ -233,8 +218,12 @@ var FTSManager = class {
233
218
  */
234
219
  async disableSearch(collectionSlug) {
235
220
  if (!isSqlite(this.db)) return;
221
+ const existing = await this.getSearchConfig(collectionSlug);
236
222
  await this.dropFtsTable(collectionSlug);
237
- await this.setSearchConfig(collectionSlug, { enabled: false });
223
+ await this.setSearchConfig(collectionSlug, {
224
+ enabled: false,
225
+ weights: existing?.weights
226
+ });
238
227
  }
239
228
  /**
240
229
  * Get index statistics for a collection
@@ -242,10 +231,10 @@ var FTSManager = class {
242
231
  async getIndexStats(collectionSlug) {
243
232
  if (!isSqlite(this.db)) return null;
244
233
  this.validateInputs(collectionSlug);
245
- const ftsTable = this.getFtsTableName(collectionSlug);
234
+ const ftsDocsizeTable = `${this.getFtsTableName(collectionSlug)}_docsize`;
246
235
  if (!await this.ftsTableExists(collectionSlug)) return null;
247
236
  return { indexed: (await sql`
248
- SELECT COUNT(*) as count FROM "${sql.raw(ftsTable)}"
237
+ SELECT COUNT(*) as count FROM "${sql.raw(ftsDocsizeTable)}"
249
238
  `.execute(this.db)).rows[0]?.count ?? 0 };
250
239
  }
251
240
  /**
@@ -258,22 +247,27 @@ var FTSManager = class {
258
247
  async verifyAndRepairIndex(collectionSlug) {
259
248
  if (!isSqlite(this.db)) return false;
260
249
  this.validateInputs(collectionSlug);
261
- const ftsTable = this.getFtsTableName(collectionSlug);
250
+ const ftsDocsizeTable = `${this.getFtsTableName(collectionSlug)}_docsize`;
262
251
  const contentTable = this.getContentTableName(collectionSlug);
263
- if (!await this.ftsTableExists(collectionSlug)) return false;
252
+ const fields = await this.getSearchableFields(collectionSlug);
253
+ const config = await this.getSearchConfig(collectionSlug);
254
+ if (!await this.ftsTableExists(collectionSlug)) {
255
+ if (!config?.enabled || fields.length === 0) return false;
256
+ console.warn(`FTS index for "${collectionSlug}" is missing. Rebuilding.`);
257
+ await this.rebuildIndex(collectionSlug, fields, config.weights);
258
+ return true;
259
+ }
264
260
  const contentCount = await sql`
265
261
  SELECT COUNT(*) as count FROM ${sql.ref(contentTable)}
266
262
  WHERE deleted_at IS NULL
267
263
  `.execute(this.db);
268
264
  const ftsCount = await sql`
269
- SELECT COUNT(*) as count FROM "${sql.raw(ftsTable)}"
265
+ SELECT COUNT(*) as count FROM "${sql.raw(ftsDocsizeTable)}"
270
266
  `.execute(this.db);
271
267
  const contentRows = contentCount.rows[0]?.count ?? 0;
272
268
  const ftsRows = ftsCount.rows[0]?.count ?? 0;
273
269
  if (contentRows !== ftsRows) {
274
270
  console.warn(`FTS index for "${collectionSlug}" has ${ftsRows} rows but content table has ${contentRows}. Rebuilding.`);
275
- const fields = await this.getSearchableFields(collectionSlug);
276
- const config = await this.getSearchConfig(collectionSlug);
277
271
  if (fields.length > 0) await this.rebuildIndex(collectionSlug, fields, config?.weights);
278
272
  return true;
279
273
  }
@@ -334,6 +328,16 @@ function isFieldType(value) {
334
328
  function isColumnType(value) {
335
329
  return COLUMN_TYPES.has(value);
336
330
  }
331
+ function parseSupports(value) {
332
+ if (!value) return [];
333
+ try {
334
+ const parsed = JSON.parse(value);
335
+ if (!Array.isArray(parsed)) return [];
336
+ return parsed.filter((item) => typeof item === "string");
337
+ } catch {
338
+ return [];
339
+ }
340
+ }
337
341
  /**
338
342
  * Error thrown when a schema operation fails
339
343
  */
@@ -387,6 +391,9 @@ var SchemaRegistry = class {
387
391
  this.validateSlug(input.slug, "collection");
388
392
  if (RESERVED_COLLECTION_SLUGS.includes(input.slug)) throw new SchemaError(`Collection slug "${input.slug}" is reserved`, "RESERVED_SLUG");
389
393
  if (await this.getCollection(input.slug)) throw new SchemaError(`Collection "${input.slug}" already exists`, "COLLECTION_EXISTS");
394
+ await this.validateUrlPattern(input.urlPattern);
395
+ await this.checkLabelUniqueness(input.label, input.labelSingular);
396
+ await this.checkUrlPatternUniqueness(input.urlPattern);
390
397
  const id = ulid();
391
398
  const hasSeo = input.hasSeo ?? input.supports?.includes("seo") ?? false;
392
399
  await withTransaction(this.db, async (trx) => {
@@ -415,26 +422,67 @@ var SchemaRegistry = class {
415
422
  async updateCollection(slug, input) {
416
423
  const existing = await this.getCollection(slug);
417
424
  if (!existing) throw new SchemaError(`Collection "${slug}" not found`, "COLLECTION_NOT_FOUND");
425
+ const labelChanged = input.label !== void 0 && input.label !== existing.label;
426
+ const labelSingularChanged = input.labelSingular !== void 0 && input.labelSingular !== (existing.labelSingular ?? void 0);
427
+ if (labelChanged || labelSingularChanged) await this.checkLabelUniqueness(labelChanged ? input.label : void 0, labelSingularChanged ? input.labelSingular : void 0, slug);
428
+ if (input.urlPattern !== void 0 && input.urlPattern !== (existing.urlPattern ?? null)) {
429
+ await this.validateUrlPattern(input.urlPattern);
430
+ await this.checkUrlPatternUniqueness(input.urlPattern, slug);
431
+ }
418
432
  const now = (/* @__PURE__ */ new Date()).toISOString();
419
433
  const supportsArray = input.supports ?? existing.supports;
420
434
  const hasSeo = input.hasSeo !== void 0 ? input.hasSeo : input.supports !== void 0 ? supportsArray.includes("seo") : existing.hasSeo;
421
- await this.db.updateTable("_dineway_collections").set({
422
- label: input.label ?? existing.label,
423
- label_singular: input.labelSingular ?? existing.labelSingular ?? null,
424
- description: input.description ?? existing.description ?? null,
425
- icon: input.icon ?? existing.icon ?? null,
426
- supports: input.supports ? JSON.stringify(input.supports) : JSON.stringify(existing.supports),
427
- url_pattern: input.urlPattern !== void 0 ? input.urlPattern ?? null : existing.urlPattern ?? null,
428
- has_seo: hasSeo ? 1 : 0,
429
- comments_enabled: input.commentsEnabled !== void 0 ? input.commentsEnabled ? 1 : 0 : existing.commentsEnabled ? 1 : 0,
430
- comments_moderation: input.commentsModeration ?? existing.commentsModeration,
431
- comments_closed_after_days: input.commentsClosedAfterDays !== void 0 ? input.commentsClosedAfterDays : existing.commentsClosedAfterDays,
432
- comments_auto_approve_users: input.commentsAutoApproveUsers !== void 0 ? input.commentsAutoApproveUsers ? 1 : 0 : existing.commentsAutoApproveUsers ? 1 : 0,
433
- updated_at: now
434
- }).where("slug", "=", slug).execute();
435
- const updated = await this.getCollection(slug);
436
- if (!updated) throw new SchemaError("Failed to update collection", "UPDATE_FAILED");
437
- return updated;
435
+ return withTransaction(this.db, async (trx) => {
436
+ await trx.updateTable("_dineway_collections").set({
437
+ label: input.label ?? existing.label,
438
+ label_singular: input.labelSingular ?? existing.labelSingular ?? null,
439
+ description: input.description ?? existing.description ?? null,
440
+ icon: input.icon ?? existing.icon ?? null,
441
+ supports: input.supports ? JSON.stringify(input.supports) : JSON.stringify(existing.supports),
442
+ url_pattern: input.urlPattern !== void 0 ? input.urlPattern ?? null : existing.urlPattern ?? null,
443
+ has_seo: hasSeo ? 1 : 0,
444
+ comments_enabled: input.commentsEnabled !== void 0 ? input.commentsEnabled ? 1 : 0 : existing.commentsEnabled ? 1 : 0,
445
+ comments_moderation: input.commentsModeration ?? existing.commentsModeration,
446
+ comments_closed_after_days: input.commentsClosedAfterDays !== void 0 ? input.commentsClosedAfterDays : existing.commentsClosedAfterDays,
447
+ comments_auto_approve_users: input.commentsAutoApproveUsers !== void 0 ? input.commentsAutoApproveUsers ? 1 : 0 : existing.commentsAutoApproveUsers ? 1 : 0,
448
+ updated_at: now
449
+ }).where("slug", "=", slug).execute();
450
+ const row = await trx.selectFrom("_dineway_collections").where("slug", "=", slug).selectAll().executeTakeFirst();
451
+ if (!row) throw new SchemaError("Failed to update collection", "UPDATE_FAILED");
452
+ if (input.supports !== void 0) {
453
+ if (existing.supports.includes("search") !== parseSupports(row.supports).includes("search")) await this.syncSearchState(slug, trx);
454
+ }
455
+ return this.mapCollectionRow(row);
456
+ });
457
+ }
458
+ async validateUrlPattern(urlPattern) {
459
+ if (!urlPattern) return;
460
+ if (!urlPattern.includes("{slug}")) throw new SchemaError("URL pattern must include a {slug} placeholder", "INVALID_URL_PATTERN");
461
+ }
462
+ async checkLabelUniqueness(label, labelSingular, excludeSlug) {
463
+ if (!label && !labelSingular) return;
464
+ let query = this.db.selectFrom("_dineway_collections").select([
465
+ "slug",
466
+ "label",
467
+ "label_singular"
468
+ ]);
469
+ if (excludeSlug) query = query.where("slug", "!=", excludeSlug);
470
+ const others = await query.execute();
471
+ for (const other of others) {
472
+ if (label && other.label === label) throw new SchemaError(`A content type with the label "${label}" already exists`, "LABEL_EXISTS");
473
+ if (labelSingular && other.label_singular === labelSingular) throw new SchemaError(`A content type with the singular label "${labelSingular}" already exists`, "LABEL_EXISTS");
474
+ }
475
+ }
476
+ async checkUrlPatternUniqueness(urlPattern, excludeSlug) {
477
+ if (!urlPattern) return;
478
+ let query = this.db.selectFrom("_dineway_collections").select([
479
+ "slug",
480
+ "label",
481
+ "url_pattern"
482
+ ]);
483
+ if (excludeSlug) query = query.where("slug", "!=", excludeSlug);
484
+ const others = await query.execute();
485
+ for (const other of others) if (other.url_pattern === urlPattern) throw new SchemaError(`The URL pattern "${urlPattern}" is already used by "${other.label}"`, "URL_PATTERN_EXISTS");
438
486
  }
439
487
  /**
440
488
  * Delete a collection
@@ -445,8 +493,11 @@ var SchemaRegistry = class {
445
493
  if (!options?.force) {
446
494
  if (await this.collectionHasContent(slug)) throw new SchemaError(`Collection "${slug}" has content. Use force: true to delete.`, "COLLECTION_HAS_CONTENT");
447
495
  }
448
- await this.dropContentTable(slug);
449
- await this.db.deleteFrom("_dineway_collections").where("id", "=", existing.id).execute();
496
+ await withTransaction(this.db, async (trx) => {
497
+ await new FTSManager(trx).dropFtsTable(slug);
498
+ await this.dropContentTable(slug, trx);
499
+ await trx.deleteFrom("_dineway_collections").where("id", "=", existing.id).execute();
500
+ });
450
501
  }
451
502
  /**
452
503
  * List fields for a collection
@@ -476,30 +527,34 @@ var SchemaRegistry = class {
476
527
  const columnType = FIELD_TYPE_TO_COLUMN[input.type];
477
528
  const maxSort = await this.db.selectFrom("_dineway_fields").where("collection_id", "=", collection.id).select((eb) => eb.fn.max("sort_order").as("max")).executeTakeFirst();
478
529
  const sortOrder = input.sortOrder ?? (maxSort?.max ?? -1) + 1;
479
- await this.db.insertInto("_dineway_fields").values({
480
- id,
481
- collection_id: collection.id,
482
- slug: input.slug,
483
- label: input.label,
484
- type: input.type,
485
- column_type: columnType,
486
- required: input.required ? 1 : 0,
487
- unique: input.unique ? 1 : 0,
488
- default_value: input.defaultValue !== void 0 ? JSON.stringify(input.defaultValue) : null,
489
- validation: input.validation ? JSON.stringify(input.validation) : null,
490
- widget: input.widget ?? null,
491
- options: input.options ? JSON.stringify(input.options) : null,
492
- sort_order: sortOrder,
493
- searchable: input.searchable ? 1 : 0,
494
- translatable: input.translatable === false ? 0 : 1
495
- }).execute();
496
- await this.addColumn(collectionSlug, input.slug, input.type, {
497
- required: input.required,
498
- defaultValue: input.defaultValue
530
+ return withTransaction(this.db, async (trx) => {
531
+ await trx.insertInto("_dineway_fields").values({
532
+ id,
533
+ collection_id: collection.id,
534
+ slug: input.slug,
535
+ label: input.label,
536
+ type: input.type,
537
+ column_type: columnType,
538
+ required: input.required ? 1 : 0,
539
+ unique: input.unique ? 1 : 0,
540
+ default_value: input.defaultValue !== void 0 ? JSON.stringify(input.defaultValue) : null,
541
+ validation: input.validation ? JSON.stringify(input.validation) : null,
542
+ widget: input.widget ?? null,
543
+ options: input.options ? JSON.stringify(input.options) : null,
544
+ sort_order: sortOrder,
545
+ searchable: input.searchable ? 1 : 0,
546
+ translatable: input.translatable === false ? 0 : 1
547
+ }).execute();
548
+ await this.addColumn(collectionSlug, input.slug, input.type, {
549
+ required: input.required,
550
+ defaultValue: input.defaultValue
551
+ }, trx);
552
+ const fieldRow = await trx.selectFrom("_dineway_fields").where("collection_id", "=", collection.id).where("slug", "=", input.slug).selectAll().executeTakeFirst();
553
+ if (!fieldRow) throw new SchemaError("Failed to create field", "CREATE_FAILED");
554
+ const field = this.mapFieldRow(fieldRow);
555
+ if (input.searchable) await this.syncSearchState(collectionSlug, trx);
556
+ return field;
499
557
  });
500
- const field = await this.getField(collectionSlug, input.slug);
501
- if (!field) throw new SchemaError("Failed to create field", "CREATE_FAILED");
502
- return field;
503
558
  }
504
559
  /**
505
560
  * Update a field
@@ -507,36 +562,43 @@ var SchemaRegistry = class {
507
562
  async updateField(collectionSlug, fieldSlug, input) {
508
563
  const field = await this.getField(collectionSlug, fieldSlug);
509
564
  if (!field) throw new SchemaError(`Field "${fieldSlug}" not found in collection "${collectionSlug}"`, "FIELD_NOT_FOUND");
510
- await this.db.updateTable("_dineway_fields").set({
511
- label: input.label ?? field.label,
512
- required: input.required !== void 0 ? input.required ? 1 : 0 : field.required ? 1 : 0,
513
- unique: input.unique !== void 0 ? input.unique ? 1 : 0 : field.unique ? 1 : 0,
514
- searchable: input.searchable !== void 0 ? input.searchable ? 1 : 0 : field.searchable ? 1 : 0,
515
- translatable: input.translatable !== void 0 ? input.translatable ? 1 : 0 : field.translatable ? 1 : 0,
516
- default_value: input.defaultValue !== void 0 ? JSON.stringify(input.defaultValue) : field.defaultValue !== void 0 ? JSON.stringify(field.defaultValue) : null,
517
- validation: input.validation ? JSON.stringify(input.validation) : field.validation ? JSON.stringify(field.validation) : null,
518
- widget: input.widget ?? field.widget ?? null,
519
- options: input.options ? JSON.stringify(input.options) : field.options ? JSON.stringify(field.options) : null,
520
- sort_order: input.sortOrder ?? field.sortOrder
521
- }).where("id", "=", field.id).execute();
522
- const updated = await this.getField(collectionSlug, fieldSlug);
523
- if (!updated) throw new SchemaError("Failed to update field", "UPDATE_FAILED");
524
- if (input.searchable !== void 0 && input.searchable !== field.searchable) await this.rebuildSearchIndex(collectionSlug);
525
- return updated;
526
- }
527
- /**
528
- * Rebuild the search index for a collection
565
+ return withTransaction(this.db, async (trx) => {
566
+ await trx.updateTable("_dineway_fields").set({
567
+ label: input.label ?? field.label,
568
+ required: input.required !== void 0 ? input.required ? 1 : 0 : field.required ? 1 : 0,
569
+ unique: input.unique !== void 0 ? input.unique ? 1 : 0 : field.unique ? 1 : 0,
570
+ searchable: input.searchable !== void 0 ? input.searchable ? 1 : 0 : field.searchable ? 1 : 0,
571
+ translatable: input.translatable !== void 0 ? input.translatable ? 1 : 0 : field.translatable ? 1 : 0,
572
+ default_value: input.defaultValue !== void 0 ? JSON.stringify(input.defaultValue) : field.defaultValue !== void 0 ? JSON.stringify(field.defaultValue) : null,
573
+ validation: input.validation ? JSON.stringify(input.validation) : field.validation ? JSON.stringify(field.validation) : null,
574
+ widget: input.widget ?? field.widget ?? null,
575
+ options: input.options ? JSON.stringify(input.options) : field.options ? JSON.stringify(field.options) : null,
576
+ sort_order: input.sortOrder ?? field.sortOrder
577
+ }).where("id", "=", field.id).execute();
578
+ const updatedRow = await trx.selectFrom("_dineway_fields").where("collection_id", "=", field.collectionId).where("slug", "=", fieldSlug).selectAll().executeTakeFirst();
579
+ if (!updatedRow) throw new SchemaError("Failed to update field", "UPDATE_FAILED");
580
+ const updated = this.mapFieldRow(updatedRow);
581
+ if (input.searchable !== void 0 && input.searchable !== field.searchable) await this.syncSearchState(collectionSlug, trx);
582
+ return updated;
583
+ });
584
+ }
585
+ /**
586
+ * Synchronize an existing FTS index with the collection's current state.
529
587
  *
530
- * Called when searchable fields change. If search is enabled for the collection,
531
- * this will rebuild the FTS table with the updated field list.
588
+ * Schema writes must not first-time enable FTS. Initial enablement stays in
589
+ * explicit seed/import/admin flows where FTS failures can be handled deliberately.
532
590
  */
533
- async rebuildSearchIndex(collectionSlug) {
534
- const ftsManager = new FTSManager(this.db);
535
- const config = await ftsManager.getSearchConfig(collectionSlug);
536
- if (!config?.enabled) return;
591
+ async syncSearchState(collectionSlug, db) {
592
+ const conn = db ?? this.db;
593
+ const ftsManager = new FTSManager(conn);
594
+ const row = await conn.selectFrom("_dineway_collections").where("slug", "=", collectionSlug).select("supports").executeTakeFirst();
595
+ if (!row) return;
596
+ const wantsSearch = parseSupports(row.supports).includes("search");
537
597
  const searchableFields = await ftsManager.getSearchableFields(collectionSlug);
538
- if (searchableFields.length === 0) await ftsManager.disableSearch(collectionSlug);
539
- else await ftsManager.rebuildIndex(collectionSlug, searchableFields, config.weights);
598
+ const config = await ftsManager.getSearchConfig(collectionSlug);
599
+ const ftsActive = config?.enabled === true;
600
+ if (wantsSearch && searchableFields.length > 0 && ftsActive) await ftsManager.rebuildIndex(collectionSlug, searchableFields, config?.weights);
601
+ else if (ftsActive && (!wantsSearch || searchableFields.length === 0)) await ftsManager.disableSearch(collectionSlug);
540
602
  }
541
603
  /**
542
604
  * Delete a field
@@ -544,8 +606,11 @@ var SchemaRegistry = class {
544
606
  async deleteField(collectionSlug, fieldSlug) {
545
607
  const field = await this.getField(collectionSlug, fieldSlug);
546
608
  if (!field) throw new SchemaError(`Field "${fieldSlug}" not found in collection "${collectionSlug}"`, "FIELD_NOT_FOUND");
547
- await this.dropColumn(collectionSlug, fieldSlug);
548
- await this.db.deleteFrom("_dineway_fields").where("id", "=", field.id).execute();
609
+ await withTransaction(this.db, async (trx) => {
610
+ await trx.deleteFrom("_dineway_fields").where("id", "=", field.id).execute();
611
+ if (field.searchable) await this.syncSearchState(collectionSlug, trx);
612
+ await this.dropColumn(collectionSlug, fieldSlug, trx);
613
+ });
549
614
  }
550
615
  /**
551
616
  * Reorder fields
@@ -615,14 +680,15 @@ var SchemaRegistry = class {
615
680
  /**
616
681
  * Drop a content table
617
682
  */
618
- async dropContentTable(slug) {
683
+ async dropContentTable(slug, db) {
619
684
  const tableName = this.getTableName(slug);
620
- await sql`DROP TABLE IF EXISTS ${sql.ref(tableName)}`.execute(this.db);
685
+ await sql`DROP TABLE IF EXISTS ${sql.ref(tableName)}`.execute(db ?? this.db);
621
686
  }
622
687
  /**
623
688
  * Add a column to a content table
624
689
  */
625
- async addColumn(collectionSlug, fieldSlug, fieldType, options) {
690
+ async addColumn(collectionSlug, fieldSlug, fieldType, options, db) {
691
+ const conn = db ?? this.db;
626
692
  const tableName = this.getTableName(collectionSlug);
627
693
  const columnType = FIELD_TYPE_TO_COLUMN[fieldType];
628
694
  const columnName = this.getColumnName(fieldSlug);
@@ -631,28 +697,28 @@ var SchemaRegistry = class {
631
697
  await sql`
632
698
  ALTER TABLE ${sql.ref(tableName)}
633
699
  ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}
634
- `.execute(this.db);
700
+ `.execute(conn);
635
701
  } else if (options?.required) {
636
702
  const defaultVal = this.getEmptyDefault(fieldType);
637
703
  await sql`
638
704
  ALTER TABLE ${sql.ref(tableName)}
639
705
  ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}
640
- `.execute(this.db);
706
+ `.execute(conn);
641
707
  } else await sql`
642
708
  ALTER TABLE ${sql.ref(tableName)}
643
709
  ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)}
644
- `.execute(this.db);
710
+ `.execute(conn);
645
711
  }
646
712
  /**
647
713
  * Drop a column from a content table
648
714
  */
649
- async dropColumn(collectionSlug, fieldSlug) {
715
+ async dropColumn(collectionSlug, fieldSlug, db) {
650
716
  const tableName = this.getTableName(collectionSlug);
651
717
  const columnName = this.getColumnName(fieldSlug);
652
718
  await sql`
653
719
  ALTER TABLE ${sql.ref(tableName)}
654
720
  DROP COLUMN ${sql.ref(columnName)}
655
- `.execute(this.db);
721
+ `.execute(db ?? this.db);
656
722
  }
657
723
  /**
658
724
  * Check if a collection has any content
@@ -672,12 +738,14 @@ var SchemaRegistry = class {
672
738
  * Get table name for a collection
673
739
  */
674
740
  getTableName(slug) {
741
+ validateIdentifier(slug, "collection slug");
675
742
  return `ec_${slug}`;
676
743
  }
677
744
  /**
678
745
  * Get column name for a field
679
746
  */
680
747
  getColumnName(slug) {
748
+ validateIdentifier(slug, "field slug");
681
749
  return slug;
682
750
  }
683
751
  /**
@@ -848,4 +916,4 @@ var SchemaRegistry = class {
848
916
  };
849
917
 
850
918
  //#endregion
851
- export { withTransaction as a, FTSManager as i, SchemaRegistry as n, registry_exports as r, SchemaError as t };
919
+ export { FTSManager as i, SchemaRegistry as n, registry_exports as r, SchemaError as t };
@@ -0,0 +1,66 @@
1
+ import { getRequestContext } from "./request-context.mjs";
2
+
3
+ //#region src/request-cache.ts
4
+ const STORE_KEY = Symbol.for("dineway:request-cache");
5
+ function getStore() {
6
+ const existing = Reflect.get(globalThis, STORE_KEY);
7
+ if (existing instanceof WeakMap) return existing;
8
+ const weakMap = /* @__PURE__ */ new WeakMap();
9
+ Reflect.set(globalThis, STORE_KEY, weakMap);
10
+ return weakMap;
11
+ }
12
+ function asTypedPromise(promise) {
13
+ return promise;
14
+ }
15
+ const store = getStore();
16
+ /**
17
+ * Return a cached promise for `key` in the current request scope.
18
+ *
19
+ * Rejected promises are evicted so later retries can re-run the query.
20
+ */
21
+ function requestCached(key, fn) {
22
+ const ctx = getRequestContext();
23
+ if (!ctx) return fn();
24
+ let cache = store.get(ctx);
25
+ if (!cache) {
26
+ cache = /* @__PURE__ */ new Map();
27
+ store.set(ctx, cache);
28
+ }
29
+ const existing = cache.get(key);
30
+ if (existing) return asTypedPromise(existing);
31
+ const promise = Promise.resolve().then(fn).catch((error) => {
32
+ cache.delete(key);
33
+ throw error;
34
+ });
35
+ cache.set(key, promise);
36
+ return promise;
37
+ }
38
+ /**
39
+ * Peek at a cached promise without inserting a new one.
40
+ */
41
+ function peekRequestCache(key) {
42
+ const ctx = getRequestContext();
43
+ if (!ctx) return void 0;
44
+ const existing = store.get(ctx)?.get(key);
45
+ return existing ? asTypedPromise(existing) : void 0;
46
+ }
47
+ /**
48
+ * Pre-populate the request cache with a resolved value.
49
+ *
50
+ * Used by hydration paths that already loaded the data in bulk and want
51
+ * narrower downstream helper calls to short-circuit without re-querying.
52
+ */
53
+ function setRequestCacheEntry(key, value) {
54
+ const ctx = getRequestContext();
55
+ if (!ctx) return;
56
+ let cache = store.get(ctx);
57
+ if (!cache) {
58
+ cache = /* @__PURE__ */ new Map();
59
+ store.set(ctx, cache);
60
+ }
61
+ if (cache.has(key)) return;
62
+ cache.set(key, Promise.resolve(value));
63
+ }
64
+
65
+ //#endregion
66
+ export { requestCached as n, setRequestCacheEntry as r, peekRequestCache as t };
@@ -1,20 +1,6 @@
1
+ import { QueryRecorder } from "./database/instrumentation.mjs";
2
+
1
3
  //#region src/request-context.d.ts
2
- /**
3
- * Dineway Request Context
4
- *
5
- * Uses AsyncLocalStorage to provide request-scoped state to query functions
6
- * without requiring explicit parameter passing. The middleware wraps next()
7
- * in als.run(), making the context available to all code during rendering.
8
- *
9
- * For logged-out users with no CMS signals (no edit cookie, no preview param),
10
- * the middleware skips ALS entirely — zero overhead for normal traffic.
11
- *
12
- * The AsyncLocalStorage instance is stored on globalThis with a Symbol key
13
- * to guarantee a singleton even when bundlers duplicate this module across
14
- * code-split chunks. Without this, Rollup/Vite may inline the module into
15
- * multiple chunks (e.g. middleware and page components), each with its own
16
- * ALS instance — breaking request-scoped state propagation.
17
- */
18
4
  interface DinewayRequestContext {
19
5
  /** Whether the current request is in visual editing mode */
20
6
  editMode: boolean;
@@ -34,6 +20,8 @@ interface DinewayRequestContext {
34
20
  * playground sidecar flows.
35
21
  */
36
22
  db?: unknown;
23
+ /** Optional query recorder for per-request instrumentation. */
24
+ queryRecorder?: QueryRecorder;
37
25
  }
38
26
  /**
39
27
  * Run a function within a Dineway request context.
@@ -1,4 +1,4 @@
1
- import { t as Database } from "./types-DkvMXalq.mjs";
1
+ import { t as Database } from "./types-DOrVigru.mjs";
2
2
  import { Kysely } from "kysely";
3
3
 
4
4
  //#region src/database/migrations/runner.d.ts