emdash 0.40.0 → 0.40.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 (200) hide show
  1. package/dist/api/route-utils.mjs +6 -6
  2. package/dist/api/schemas/index.d.mts +1 -1
  3. package/dist/{api-cFFZpyS0.mjs → api-CuyNEHZG.mjs} +3 -3
  4. package/dist/{api-cFFZpyS0.mjs.map → api-CuyNEHZG.mjs.map} +1 -1
  5. package/dist/{apply-CoT603H3.mjs → apply-npY1TAhG.mjs} +237 -34
  6. package/dist/apply-npY1TAhG.mjs.map +1 -0
  7. package/dist/astro/index.d.mts +3 -3
  8. package/dist/astro/index.mjs +9 -9
  9. package/dist/astro/index.mjs.map +1 -1
  10. package/dist/astro/middleware/auth.d.mts +3 -3
  11. package/dist/astro/middleware/redirect.mjs +1 -1
  12. package/dist/astro/middleware.d.mts +3 -3
  13. package/dist/astro/middleware.d.mts.map +1 -1
  14. package/dist/astro/middleware.mjs +19 -18
  15. package/dist/astro/middleware.mjs.map +1 -1
  16. package/dist/astro/routes/api/admin/media-usage/progress.mjs +2 -2
  17. package/dist/astro/routes/api/admin/media-usage/repair.mjs +2 -2
  18. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +8 -8
  19. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +8 -8
  20. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +8 -8
  21. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +11 -11
  22. package/dist/astro/routes/api/admin/plugins/index.mjs +8 -8
  23. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +8 -8
  24. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +11 -11
  25. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +8 -8
  26. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +8 -8
  27. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +12 -12
  28. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +9 -9
  29. package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +9 -9
  30. package/dist/astro/routes/api/admin/plugins/updates.mjs +8 -8
  31. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +8 -8
  32. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +8 -8
  33. package/dist/astro/routes/api/admin/transfer/approvals/_id_/approve.mjs +3 -3
  34. package/dist/astro/routes/api/admin/transfer/approvals/_id_/deny.mjs +3 -3
  35. package/dist/astro/routes/api/admin/transfer/approvals/index.mjs +3 -3
  36. package/dist/astro/routes/api/admin/transfer/capabilities.mjs +3 -3
  37. package/dist/astro/routes/api/admin/transfer/exports/_id_/advance.mjs +3 -3
  38. package/dist/astro/routes/api/admin/transfer/exports/_id_/archive.mjs +3 -3
  39. package/dist/astro/routes/api/admin/transfer/exports/_id_/files/_...path_.mjs +3 -3
  40. package/dist/astro/routes/api/admin/transfer/exports/_id_/index.mjs +3 -3
  41. package/dist/astro/routes/api/admin/transfer/exports/_id_/manifest.mjs +3 -3
  42. package/dist/astro/routes/api/admin/transfer/exports/index.mjs +3 -3
  43. package/dist/astro/routes/api/admin/transfer/imports/_id_/abandon.mjs +3 -3
  44. package/dist/astro/routes/api/admin/transfer/imports/_id_/advance.mjs +3 -3
  45. package/dist/astro/routes/api/admin/transfer/imports/_id_/analyze.mjs +3 -3
  46. package/dist/astro/routes/api/admin/transfer/imports/_id_/cancel.mjs +3 -3
  47. package/dist/astro/routes/api/admin/transfer/imports/_id_/execute.mjs +3 -3
  48. package/dist/astro/routes/api/admin/transfer/imports/_id_/files/_...path_.mjs +3 -3
  49. package/dist/astro/routes/api/admin/transfer/imports/_id_/index.mjs +3 -3
  50. package/dist/astro/routes/api/admin/transfer/imports/_id_/missing.mjs +3 -3
  51. package/dist/astro/routes/api/admin/transfer/imports/_id_/plan.mjs +3 -3
  52. package/dist/astro/routes/api/admin/transfer/imports/_id_/receipt.mjs +3 -3
  53. package/dist/astro/routes/api/admin/transfer/imports/index.mjs +3 -3
  54. package/dist/astro/routes/api/dashboard.mjs +2 -2
  55. package/dist/astro/routes/api/health.mjs +1 -1
  56. package/dist/astro/routes/api/import/probe.mjs +3 -3
  57. package/dist/astro/routes/api/import/wordpress/execute.d.mts +3 -3
  58. package/dist/astro/routes/api/import/wordpress/media.d.mts +3 -3
  59. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +3 -3
  60. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +3 -3
  61. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +3 -3
  62. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +3 -3
  63. package/dist/astro/routes/api/manifest.mjs +1 -1
  64. package/dist/astro/routes/api/mcp.mjs +13 -33
  65. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  66. package/dist/astro/routes/api/media/_id_/confirm.mjs +1 -1
  67. package/dist/astro/routes/api/media/_id_/upload.mjs +1 -1
  68. package/dist/astro/routes/api/media/_id_/usage.mjs +2 -2
  69. package/dist/astro/routes/api/media/_id_.mjs +2 -2
  70. package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs +1 -1
  71. package/dist/astro/routes/api/media/upload-url.mjs +1 -1
  72. package/dist/astro/routes/api/media.mjs +2 -2
  73. package/dist/astro/routes/api/redirects/404s/index.mjs +2 -2
  74. package/dist/astro/routes/api/redirects/404s/summary.mjs +2 -2
  75. package/dist/astro/routes/api/redirects/_id_.mjs +2 -2
  76. package/dist/astro/routes/api/redirects/index.mjs +2 -2
  77. package/dist/astro/routes/api/schema/block-types/_slug_/index.mjs +8 -8
  78. package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.mjs +8 -8
  79. package/dist/astro/routes/api/schema/block-types/index.mjs +8 -8
  80. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +8 -8
  81. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +8 -8
  82. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +8 -8
  83. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +8 -8
  84. package/dist/astro/routes/api/schema/collections/index.mjs +8 -8
  85. package/dist/astro/routes/api/schema/collections/reorder.mjs +8 -8
  86. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +8 -8
  87. package/dist/astro/routes/api/schema/orphans/index.mjs +8 -8
  88. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +2 -2
  89. package/dist/astro/routes/api/settings/backups/archives/index.mjs +2 -2
  90. package/dist/astro/routes/api/settings/backups/export.mjs +2 -2
  91. package/dist/astro/routes/api/settings/backups/index.mjs +2 -2
  92. package/dist/astro/routes/api/settings.mjs +3 -3
  93. package/dist/astro/routes/api/setup/dev-bypass.mjs +4 -4
  94. package/dist/astro/routes/api/setup/index.d.mts.map +1 -1
  95. package/dist/astro/routes/api/setup/index.mjs +35 -10
  96. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  97. package/dist/astro/routes/api/snapshot.mjs +2 -2
  98. package/dist/astro/routes/api/snapshot.mjs.map +1 -1
  99. package/dist/astro/routes/robots.txt.mjs +2 -2
  100. package/dist/astro/routes/sitemap-_collection_.xml.mjs +2 -2
  101. package/dist/astro/routes/sitemap.xml.mjs +2 -2
  102. package/dist/astro/types.d.mts +7 -3
  103. package/dist/astro/types.d.mts.map +1 -1
  104. package/dist/{backup-CBu0LrXf.mjs → backup-BQRj3afm.mjs} +3 -3
  105. package/dist/{backup-CBu0LrXf.mjs.map → backup-BQRj3afm.mjs.map} +1 -1
  106. package/dist/cli/index.mjs +6 -6
  107. package/dist/client/index.d.mts +14 -14
  108. package/dist/{config-5ii2haYu.d.mts → config-BjEm_3xL.d.mts} +25 -16
  109. package/dist/config-BjEm_3xL.d.mts.map +1 -0
  110. package/dist/{context-BeGccdUX.mjs → context-BV_7t-Ua.mjs} +6 -6
  111. package/dist/{context-BeGccdUX.mjs.map → context-BV_7t-Ua.mjs.map} +1 -1
  112. package/dist/{dashboard-DjrB-xxZ.mjs → dashboard-BP-iJqpd.mjs} +2 -2
  113. package/dist/{dashboard-DjrB-xxZ.mjs.map → dashboard-BP-iJqpd.mjs.map} +1 -1
  114. package/dist/db/libsql-migrations.mjs +2 -2
  115. package/dist/db/postgres-migrations.mjs +2 -2
  116. package/dist/db/sqlite-migrations.mjs +2 -2
  117. package/dist/{direct-executor-BmuFJJZV.mjs → direct-executor-D2lNadwo.mjs} +2 -2
  118. package/dist/{direct-executor-BmuFJJZV.mjs.map → direct-executor-D2lNadwo.mjs.map} +1 -1
  119. package/dist/{emdash-runtime-DAjghyEG.mjs → emdash-runtime-CtfnZmPi.mjs} +56 -56
  120. package/dist/emdash-runtime-CtfnZmPi.mjs.map +1 -0
  121. package/dist/{entry-lock-DVhCt49A.d.mts → entry-lock-Cf5OoIgN.d.mts} +18 -18
  122. package/dist/{entry-lock-DVhCt49A.d.mts.map → entry-lock-Cf5OoIgN.d.mts.map} +1 -1
  123. package/dist/{hreflang-DMFBL2eb.mjs → hreflang-CbVI4paR.mjs} +2 -2
  124. package/dist/{hreflang-DMFBL2eb.mjs.map → hreflang-CbVI4paR.mjs.map} +1 -1
  125. package/dist/{identity-IyNWwXr6.mjs → identity-Be5kzutJ.mjs} +2 -2
  126. package/dist/{identity-IyNWwXr6.mjs.map → identity-Be5kzutJ.mjs.map} +1 -1
  127. package/dist/{import-C3JZplWq.mjs → import-YzfKCaco.mjs} +2 -2
  128. package/dist/{import-C3JZplWq.mjs.map → import-YzfKCaco.mjs.map} +1 -1
  129. package/dist/index.d.mts +4 -4
  130. package/dist/index.mjs +16 -16
  131. package/dist/{manifest-builder-B8zR-73J.mjs → manifest-builder-BmSAOOEF.mjs} +2 -2
  132. package/dist/{manifest-builder-B8zR-73J.mjs.map → manifest-builder-BmSAOOEF.mjs.map} +1 -1
  133. package/dist/media/local-runtime.d.mts +3 -3
  134. package/dist/media/local-runtime.mjs +3 -3
  135. package/dist/{media-Qp5S3eH3.mjs → media-CPXTbb1R.mjs} +20 -2
  136. package/dist/media-CPXTbb1R.mjs.map +1 -0
  137. package/dist/{media-h7tOiDLj.mjs → media-JXRWDhw3.mjs} +77 -4
  138. package/dist/media-JXRWDhw3.mjs.map +1 -0
  139. package/dist/{media-usage-My-Vi-ex.mjs → media-usage-De-zeR9E.mjs} +2 -2
  140. package/dist/{media-usage-My-Vi-ex.mjs.map → media-usage-De-zeR9E.mjs.map} +1 -1
  141. package/dist/{menus-TsSGmSR4.mjs → menus-Cm68rsAq.mjs} +4 -4
  142. package/dist/{menus-TsSGmSR4.mjs.map → menus-Cm68rsAq.mjs.map} +1 -1
  143. package/dist/migrations/index.mjs +2 -2
  144. package/dist/page/index.mjs +2 -2
  145. package/dist/plugin-test-runtime.d.mts +3 -3
  146. package/dist/plugin-test-runtime.mjs +18 -18
  147. package/dist/plugin-utils.d.mts +3 -3
  148. package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
  149. package/dist/plugins/host.d.mts +1 -1
  150. package/dist/plugins/host.mjs +6 -6
  151. package/dist/{redirect-iJf5fltD.mjs → redirect-B29UjdUn.mjs} +10 -14
  152. package/dist/redirect-B29UjdUn.mjs.map +1 -0
  153. package/dist/{redirects-CYu1u1aK.mjs → redirects-Ca2MPfEG.mjs} +2 -2
  154. package/dist/{redirects-CYu1u1aK.mjs.map → redirects-Ca2MPfEG.mjs.map} +1 -1
  155. package/dist/{routes-CUsM40Cc.mjs → routes-vrR95WR6.mjs} +2 -2
  156. package/dist/{routes-CUsM40Cc.mjs.map → routes-vrR95WR6.mjs.map} +1 -1
  157. package/dist/runtime.d.mts +3 -3
  158. package/dist/seed/index.mjs +4 -4
  159. package/dist/seo/index.mjs +3 -3
  160. package/dist/{settings-CgjRWGW9.mjs → settings-B4vDl1fK.mjs} +2 -2
  161. package/dist/{settings-CgjRWGW9.mjs.map → settings-B4vDl1fK.mjs.map} +1 -1
  162. package/dist/{settings-2xNYr90o.mjs → settings-Dyj6v8C4.mjs} +2 -2
  163. package/dist/{settings-2xNYr90o.mjs.map → settings-Dyj6v8C4.mjs.map} +1 -1
  164. package/dist/{snapshot-CugwUqKQ.mjs → snapshot-PawoX-WG.mjs} +180 -3
  165. package/dist/snapshot-PawoX-WG.mjs.map +1 -0
  166. package/dist/{storage-query-Bdv7A62k.d.mts → storage-query-FXJH5pI8.d.mts} +6 -1
  167. package/dist/storage-query-FXJH5pI8.d.mts.map +1 -0
  168. package/dist/{transfer-BhUCrgDc.mjs → transfer-DgeCvHnj.mjs} +4 -4
  169. package/dist/{transfer-BhUCrgDc.mjs.map → transfer-DgeCvHnj.mjs.map} +1 -1
  170. package/dist/validate-CM79QXYE.d.mts.map +1 -1
  171. package/dist/version-DXsxRrob.mjs +7 -0
  172. package/dist/{version-DUQDHgrf.mjs.map → version-DXsxRrob.mjs.map} +1 -1
  173. package/package.json +5 -5
  174. package/src/api/handlers/index.ts +1 -0
  175. package/src/api/handlers/media-upload.ts +17 -71
  176. package/src/api/handlers/media.ts +97 -3
  177. package/src/api/handlers/snapshot.ts +257 -3
  178. package/src/astro/integration/vite-config.ts +10 -4
  179. package/src/astro/middleware.ts +1 -0
  180. package/src/astro/routes/api/setup/index.ts +66 -23
  181. package/src/astro/routes/api/snapshot.ts +2 -1
  182. package/src/astro/types.ts +5 -0
  183. package/src/cleanup.ts +10 -0
  184. package/src/components/Columns.astro +3 -1
  185. package/src/components/Cover.astro +3 -1
  186. package/src/database/repositories/media.ts +40 -1
  187. package/src/database/repositories/redirect.ts +17 -34
  188. package/src/emdash-runtime.ts +28 -0
  189. package/src/index.ts +1 -0
  190. package/src/mcp/server.ts +13 -48
  191. package/src/seed/apply.ts +322 -41
  192. package/dist/apply-CoT603H3.mjs.map +0 -1
  193. package/dist/config-5ii2haYu.d.mts.map +0 -1
  194. package/dist/emdash-runtime-DAjghyEG.mjs.map +0 -1
  195. package/dist/media-Qp5S3eH3.mjs.map +0 -1
  196. package/dist/media-h7tOiDLj.mjs.map +0 -1
  197. package/dist/redirect-iJf5fltD.mjs.map +0 -1
  198. package/dist/snapshot-CugwUqKQ.mjs.map +0 -1
  199. package/dist/storage-query-Bdv7A62k.d.mts.map +0 -1
  200. package/dist/version-DUQDHgrf.mjs +0 -7
@@ -3,10 +3,11 @@ import { o as resolveConfiguredLocale, r as getI18nConfig } from "./config-CvmjJ
3
3
  import { t as OptionsRepository } from "./options-DhrI7eMv.mjs";
4
4
  import { i as RevisionRepository, t as ContentRepository } from "./content-DFaIEDj9.mjs";
5
5
  import { r as coalesceObjectCacheWrites } from "./object-cache-CqWNY5uP.mjs";
6
+ import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-Dms7Uxbt.mjs";
6
7
  import { t as withTransaction } from "./transaction-BcJO4Tmc.mjs";
7
- import { t as MediaRepository } from "./media-Qp5S3eH3.mjs";
8
+ import { t as MediaRepository } from "./media-CPXTbb1R.mjs";
8
9
  import { t as TaxonomyRepository } from "./taxonomy-C3Fu1YeH.mjs";
9
- import { t as RedirectRepository } from "./redirect-iJf5fltD.mjs";
10
+ import { t as RedirectRepository } from "./redirect-B29UjdUn.mjs";
10
11
  import { t as BylineRepository } from "./byline-DYmkXqvo.mjs";
11
12
  import { t as INTERNAL_MEDIA_PREFIX } from "./normalize-CO2P8-iE.mjs";
12
13
  import { t as imageSize } from "./dist-BNwANFJR.mjs";
@@ -14,7 +15,7 @@ import { t as FTSManager } from "./fts-manager-S-K0laoS.mjs";
14
15
  import { d as findTaxonomyStructure, p as saveTaxonomyStructure } from "./loader-BzqeR1ZN.mjs";
15
16
  import { U as markContentMediaUsageCollectionStaleSafely, a as SchemaRegistry, t as BlockTypeRegistry } from "./block-type-registry-DUI_3KxT.mjs";
16
17
  import { i as resolveBlockTypes, r as normalizeBlocksData } from "./block-values-C_VIPr5t.mjs";
17
- import { c as setSiteSettings, s as invalidateSiteSettingsCache } from "./settings-CgjRWGW9.mjs";
18
+ import { c as setSiteSettings, s as invalidateSiteSettingsCache } from "./settings-B4vDl1fK.mjs";
18
19
  import { a as ssrfSafeFetch, s as validateExternalUrl } from "./ssrf-CJUnJymJ.mjs";
19
20
  import { n as validateSeed, t as findTaxonomyStructureSource } from "./validate-CHosPEhs.mjs";
20
21
  import { ulid } from "ulidx";
@@ -131,7 +132,10 @@ function isRecord(value) {
131
132
  * This is the core implementation that bootstraps an EmDash site from a seed file.
132
133
  * Apply order is critical for foreign keys and references.
133
134
  */
134
- var apply_exports = /* @__PURE__ */ __exportAll({ applySeed: () => applySeed });
135
+ var apply_exports = /* @__PURE__ */ __exportAll({
136
+ applySeed: () => applySeed,
137
+ applySeedWithinBudget: () => applySeedWithinBudget
138
+ });
135
139
  async function applySiteSettings(db, settings, onConflict, result) {
136
140
  const entries = Object.entries(settings).filter(([, value]) => value !== void 0);
137
141
  if (entries.length === 0) return;
@@ -203,12 +207,59 @@ function isUntouchedBuiltInTaxonomyDef(def) {
203
207
  * @returns Result summary
204
208
  */
205
209
  async function applySeed(db, seed, options = {}) {
206
- return coalesceObjectCacheWrites(() => applySeedWrites(db, seed, options));
210
+ const { result } = await coalesceObjectCacheWrites(() => applySeedWrites(db, seed, options, null));
211
+ return result;
207
212
  }
208
- async function applySeedWrites(db, seed, options) {
213
+ /**
214
+ * Apply a seed over several calls, for a platform that caps the queries or
215
+ * fetches of one request.
216
+ *
217
+ * The limits are thresholds, checked before each taxonomy term, byline and
218
+ * content entry is created and before the phases that follow content. Schema,
219
+ * taxonomy definitions and settings are applied in every call, the item that
220
+ * crosses a threshold finishes, and the phases after content (menus,
221
+ * redirects, widgets, sections) run in one call, so set the limits well below
222
+ * the platform's. A call that stops returns `complete: false`; applying the
223
+ * same seed again continues with the first item not yet created. A call
224
+ * creates at least one item before it stops, so repeated calls finish. A
225
+ * `$media` URL used by entries in different calls is downloaded and stored
226
+ * again, as a new media row, in each of those calls. Requires
227
+ * `onConflict: "skip"`.
228
+ */
229
+ async function applySeedWithinBudget(db, seed, options, limits) {
230
+ const budget = new SeedBudget(limits);
231
+ return coalesceObjectCacheWrites(() => applySeedWrites(db.withPlugin(budget), seed, options, budget));
232
+ }
233
+ /**
234
+ * Counts what a budgeted call spends. As a Kysely plugin it sees every query
235
+ * the call runs, including those inside transactions.
236
+ */
237
+ var SeedBudget = class {
238
+ #queries = 0;
239
+ #mediaDownloads = 0;
240
+ constructor(limits) {
241
+ this.limits = limits;
242
+ }
243
+ transformQuery(args) {
244
+ this.#queries++;
245
+ return args.node;
246
+ }
247
+ transformResult(args) {
248
+ return Promise.resolve(args.result);
249
+ }
250
+ countMediaDownload() {
251
+ this.#mediaDownloads++;
252
+ }
253
+ isSpent() {
254
+ const { queries, mediaDownloads } = this.limits;
255
+ return queries !== void 0 && this.#queries >= queries || mediaDownloads !== void 0 && this.#mediaDownloads >= mediaDownloads;
256
+ }
257
+ };
258
+ async function applySeedWrites(db, seed, options, budget) {
209
259
  const validation = validateSeed(seed);
210
260
  if (!validation.valid) throw new Error(`Invalid seed file:\n${validation.errors.join("\n")}`);
211
261
  const { includeContent = false, storage, skipMediaDownload = false, onConflict = "skip" } = options;
262
+ if (budget && onConflict !== "skip") throw new Error("A seed budget requires onConflict: \"skip\"");
212
263
  const result = {
213
264
  blockTypes: {
214
265
  created: 0,
@@ -264,11 +315,18 @@ async function applySeedWrites(db, seed, options) {
264
315
  skipped: 0
265
316
  }
266
317
  };
318
+ let complete = true;
319
+ const progress = {
320
+ done: 0,
321
+ total: countSeedItems(seed, includeContent)
322
+ };
323
+ const mayCreateItem = () => !budget || result.taxonomies.terms + result.bylines.created + result.content.created === 0 || !budget.isSpent();
267
324
  const mediaContext = {
268
325
  db,
269
326
  storage: storage ?? null,
270
327
  skipMediaDownload,
271
- mediaCache: /* @__PURE__ */ new Map()
328
+ mediaCache: /* @__PURE__ */ new Map(),
329
+ budget
272
330
  };
273
331
  const seedIdMap = /* @__PURE__ */ new Map();
274
332
  const seedBylineIdMap = /* @__PURE__ */ new Map();
@@ -441,21 +499,31 @@ async function applySeedWrites(db, seed, options) {
441
499
  }
442
500
  if (includeContent && taxonomy.terms && taxonomy.terms.length > 0) {
443
501
  const termRepo = new TaxonomyRepository(db);
444
- if (structure.hierarchical) await applyHierarchicalTerms(termRepo, taxonomy.name, defLocale, taxonomy.terms, termSeedIdMap, result, onConflict);
445
- else for (const term of taxonomy.terms) {
502
+ const existingTerms = await findExistingSeedTerms(db, taxonomy.name, taxonomy.terms);
503
+ if (structure.hierarchical) {
504
+ const applied = await applyHierarchicalTerms(termRepo, taxonomy.name, defLocale, taxonomy.terms, existingTerms, termSeedIdMap, result, onConflict, mayCreateItem);
505
+ progress.done += applied.processed;
506
+ complete = applied.complete;
507
+ } else for (const term of taxonomy.terms) {
446
508
  const termLocale = resolveConfiguredLocale(term.locale ?? defLocale);
447
- const existing = await termRepo.findBySlug(taxonomy.name, term.slug, termLocale);
448
- if (existing) {
509
+ const termKey = seedTermKey(termLocale, term.slug);
510
+ const existingId = existingTerms.get(termKey);
511
+ if (existingId) {
449
512
  if (onConflict === "error") throw new Error(`Conflict: taxonomy term "${term.slug}" in "${taxonomy.name}" (${termLocale}) already exists`);
450
513
  if (onConflict === "update") {
451
- await termRepo.update(existing.id, {
514
+ await termRepo.update(existingId, {
452
515
  label: term.label,
453
516
  data: term.description ? { description: term.description } : {}
454
517
  });
455
518
  result.taxonomies.terms++;
456
519
  }
457
- if (term.id) termSeedIdMap.set(term.id, existing.id);
520
+ if (term.id) termSeedIdMap.set(term.id, existingId);
521
+ progress.done++;
458
522
  } else {
523
+ if (!mayCreateItem()) {
524
+ complete = false;
525
+ break;
526
+ }
459
527
  const translationOf = term.translationOf ? termSeedIdMap.get(term.translationOf) : void 0;
460
528
  const created = await termRepo.create({
461
529
  name: taxonomy.name,
@@ -465,25 +533,29 @@ async function applySeedWrites(db, seed, options) {
465
533
  locale: termLocale,
466
534
  translationOf
467
535
  });
536
+ existingTerms.set(termKey, created.id);
468
537
  if (term.id) termSeedIdMap.set(term.id, created.id);
469
538
  result.taxonomies.terms++;
539
+ progress.done++;
470
540
  }
471
541
  }
472
542
  }
543
+ if (!complete) break;
473
544
  }
474
545
  const { invalidateTaxonomyDefsCache } = await import("./taxonomies-C1IwNe8Q.mjs").then((n) => n.f);
475
546
  invalidateTaxonomyDefsCache();
476
547
  }
477
- if (includeContent && seed.bylines) {
548
+ if (complete && includeContent && seed.bylines) {
478
549
  const bylineRepo = new BylineRepository(db);
550
+ const existingBylines = await findExistingSeedBylines(db, seed.bylines.map((byline) => byline.slug));
479
551
  for (const byline of seed.bylines) {
480
- const existing = await bylineRepo.findBySlug(byline.slug);
481
- if (existing) {
552
+ const existingId = existingBylines.get(byline.slug);
553
+ if (existingId) {
482
554
  if (onConflict === "error") throw new Error(`Conflict: byline "${byline.slug}" already exists`);
483
555
  if (onConflict === "update") {
484
556
  const avatar = byline.avatar ? await resolveSeedBylineAvatar(db, byline.avatar) : null;
485
557
  try {
486
- if (!await bylineRepo.update(existing.id, {
558
+ if (!await bylineRepo.update(existingId, {
487
559
  displayName: byline.displayName,
488
560
  bio: byline.bio ?? null,
489
561
  websiteUrl: byline.websiteUrl ?? null,
@@ -494,15 +566,21 @@ async function applySeedWrites(db, seed, options) {
494
566
  if (avatar?.created) await deleteMediaRow(db, avatar.id);
495
567
  throw error;
496
568
  }
497
- seedBylineIdMap.set(byline.id, existing.id);
569
+ seedBylineIdMap.set(byline.id, existingId);
498
570
  result.bylines.updated++;
499
571
  if (avatar?.created) result.media.created++;
572
+ progress.done++;
500
573
  continue;
501
574
  }
502
- seedBylineIdMap.set(byline.id, existing.id);
575
+ seedBylineIdMap.set(byline.id, existingId);
503
576
  result.bylines.skipped++;
577
+ progress.done++;
504
578
  continue;
505
579
  }
580
+ if (!mayCreateItem()) {
581
+ complete = false;
582
+ break;
583
+ }
506
584
  const avatar = byline.avatar ? await resolveSeedBylineAvatar(db, byline.avatar) : null;
507
585
  let createdId;
508
586
  try {
@@ -518,12 +596,14 @@ async function applySeedWrites(db, seed, options) {
518
596
  if (avatar?.created) await deleteMediaRow(db, avatar.id);
519
597
  throw error;
520
598
  }
599
+ existingBylines.set(byline.slug, createdId);
521
600
  seedBylineIdMap.set(byline.id, createdId);
522
601
  result.bylines.created++;
523
602
  if (avatar?.created) result.media.created++;
603
+ progress.done++;
524
604
  }
525
605
  }
526
- if (includeContent && seed.content) {
606
+ if (complete && includeContent && seed.content) {
527
607
  const contentRepo = new ContentRepository(db);
528
608
  const schemaRegistry = new SchemaRegistry(db);
529
609
  const datetimeContexts = /* @__PURE__ */ new Map();
@@ -532,10 +612,11 @@ async function applySeedWrites(db, seed, options) {
532
612
  const collectionInfo = await schemaRegistry.getCollectionWithFields(collectionSlug);
533
613
  const collectionRoutable = collectionInfo?.routable !== false;
534
614
  const resolvedBlockTypes = collectionInfo?.fields.some((field) => field.type === "blocks") ? await resolveBlockTypes(db) : void 0;
615
+ const existingEntries = await findExistingSeedEntries(contentRepo, collectionSlug, entries, defaultLocale);
535
616
  for (const entry of entries) {
536
- const entrySlug = typeof entry.slug === "string" && entry.slug.trim().length > 0 ? entry.slug : null;
537
- const entryLocale = resolveConfiguredLocale(entry.locale ?? defaultLocale);
538
- const existing = entrySlug ? await contentRepo.findBySlug(collectionSlug, entrySlug, entryLocale) : await contentRepo.findById(collectionSlug, entry.id);
617
+ const { slug: entrySlug, locale: entryLocale } = seedEntryIdentity(entry, defaultLocale);
618
+ const entryKey = seedEntryKey(entry, entrySlug, entryLocale);
619
+ const existing = existingEntries.get(entryKey);
539
620
  if (existing) {
540
621
  if (onConflict === "error") throw new Error(`Conflict: content "${entrySlug ?? entry.id}" in "${collectionSlug}" already exists`);
541
622
  if (onConflict === "update") {
@@ -580,13 +661,19 @@ async function applySeedWrites(db, seed, options) {
580
661
  }
581
662
  seedIdMap.set(entry.id, existing.id);
582
663
  result.content.updated++;
664
+ progress.done++;
583
665
  await markSeedContentCollectionStale(collectionSlug);
584
666
  continue;
585
667
  }
586
668
  result.content.skipped++;
587
669
  seedIdMap.set(entry.id, existing.id);
670
+ progress.done++;
588
671
  continue;
589
672
  }
673
+ if (!mayCreateItem()) {
674
+ complete = false;
675
+ break;
676
+ }
590
677
  let resolvedData = await resolveReferences(entry.data, seedIdMap, mediaContext, result);
591
678
  if (collectionInfo) resolvedData = await normalizeBlocksData(db, collectionInfo, resolvedData, {}, { restoreBlocks: true }, false, resolvedBlockTypes);
592
679
  let translationOf;
@@ -623,14 +710,26 @@ async function applySeedWrites(db, seed, options) {
623
710
  throw error;
624
711
  }
625
712
  seedIdMap.set(entry.id, created.id);
713
+ existingEntries.set(entryKey, created);
626
714
  result.content.created++;
715
+ progress.done++;
627
716
  await markSeedContentCollectionStale(collectionSlug);
628
717
  }
718
+ if (!complete) break;
629
719
  }
630
720
  } finally {
631
721
  await retryFailedSeedContentStaleMarks();
632
722
  }
633
723
  }
724
+ if (complete && !mayCreateItem()) complete = false;
725
+ if (!complete) {
726
+ await invalidateSeedCaches();
727
+ return {
728
+ result,
729
+ complete,
730
+ progress
731
+ };
732
+ }
634
733
  if (seed.menus) {
635
734
  const menuSeedIdMap = /* @__PURE__ */ new Map();
636
735
  const itemSeedIdMap = /* @__PURE__ */ new Map();
@@ -767,18 +866,110 @@ async function applySeedWrites(db, seed, options) {
767
866
  }
768
867
  }
769
868
  }
869
+ await invalidateSeedCaches();
870
+ return {
871
+ result,
872
+ complete,
873
+ progress
874
+ };
875
+ }
876
+ function countSeedItems(seed, includeContent) {
877
+ if (!includeContent) return 0;
878
+ const terms = (seed.taxonomies ?? []).reduce((total, taxonomy) => total + (taxonomy.terms?.length ?? 0), 0);
879
+ const entries = Object.values(seed.content ?? {}).reduce((total, collectionEntries) => total + collectionEntries.length, 0);
880
+ return terms + (seed.bylines?.length ?? 0) + entries;
881
+ }
882
+ function seedTermKey(locale, slug) {
883
+ return `${locale}::${slug}`;
884
+ }
885
+ /**
886
+ * Find which of a taxonomy's seed terms already exist, as term ids keyed by
887
+ * `seedTermKey`. Each query covers a batch of slugs.
888
+ */
889
+ async function findExistingSeedTerms(db, taxonomyName, terms) {
890
+ const existing = /* @__PURE__ */ new Map();
891
+ for (const batch of chunks([...new Set(terms.map((term) => term.slug))], SQL_BATCH_SIZE)) {
892
+ const rows = await db.selectFrom("taxonomies").select([
893
+ "id",
894
+ "slug",
895
+ "locale"
896
+ ]).where("name", "=", taxonomyName).where("slug", "in", batch).execute();
897
+ for (const row of rows) existing.set(seedTermKey(row.locale, row.slug), row.id);
898
+ }
899
+ return existing;
900
+ }
901
+ /**
902
+ * Find which seed bylines already exist, as byline ids keyed by slug. A slug
903
+ * in several locales resolves to the lowest locale code, as
904
+ * `BylineRepository.findBySlug` does. Each query covers a batch of slugs.
905
+ */
906
+ async function findExistingSeedBylines(db, slugs) {
907
+ const existing = /* @__PURE__ */ new Map();
908
+ for (const batch of chunks([...new Set(slugs)], SQL_BATCH_SIZE)) {
909
+ const rows = await db.selectFrom("_emdash_bylines").select(["id", "slug"]).where("slug", "in", batch).orderBy("locale", "asc").execute();
910
+ for (const row of rows) if (!existing.has(row.slug)) existing.set(row.slug, row.id);
911
+ }
912
+ return existing;
913
+ }
914
+ /**
915
+ * Invalidate caches that may have been affected by seed data.
916
+ * Seed creates bylines, redirects, and collections, all of which
917
+ * have module-level caches in the hot path.
918
+ */
919
+ async function invalidateSeedCaches() {
770
920
  const { invalidateBylineCache } = await import("./bylines-BPwJqoYH.mjs").then((n) => n.t);
771
921
  const { invalidateRedirectCache } = await import("./cache-m7UEkRFV.mjs").then((n) => n.t);
772
922
  const { invalidateUrlPatternCache } = await import("./query-B48peoRT.mjs").then((n) => n.o);
773
923
  invalidateBylineCache();
774
924
  invalidateRedirectCache();
775
925
  invalidateUrlPatternCache();
776
- return result;
926
+ }
927
+ function seedEntryIdentity(entry, defaultLocale) {
928
+ return {
929
+ slug: typeof entry.slug === "string" && entry.slug.trim().length > 0 ? entry.slug : null,
930
+ locale: resolveConfiguredLocale(entry.locale ?? defaultLocale)
931
+ };
777
932
  }
778
933
  /**
779
- * Apply hierarchical taxonomy terms (parents before children)
934
+ * Slugful entries use the existing locale-aware key. Slugless seed entries
935
+ * persist their seed ID, which keeps re-application idempotent.
780
936
  */
781
- async function applyHierarchicalTerms(termRepo, taxonomyName, defLocale, terms, termSeedIdMap, result, onConflict = "skip") {
937
+ function seedEntryKey(entry, slug, locale) {
938
+ return slug === null ? `id:${entry.id}` : `slug:${locale}:${slug}`;
939
+ }
940
+ /**
941
+ * Find which of a collection's seed entries already exist, keyed by
942
+ * `seedEntryKey`. Each query covers a batch of entries.
943
+ */
944
+ async function findExistingSeedEntries(repo, collectionSlug, entries, defaultLocale) {
945
+ const identities = entries.map((entry) => ({
946
+ entry,
947
+ ...seedEntryIdentity(entry, defaultLocale)
948
+ }));
949
+ const slugsByLocale = /* @__PURE__ */ new Map();
950
+ for (const { slug, locale } of identities) {
951
+ if (slug === null) continue;
952
+ const slugs = slugsByLocale.get(locale);
953
+ if (slugs) slugs.push(slug);
954
+ else slugsByLocale.set(locale, [slug]);
955
+ }
956
+ const bySlug = /* @__PURE__ */ new Map();
957
+ for (const [locale, slugs] of slugsByLocale) bySlug.set(locale, await repo.findManyBySlugsInLocale(collectionSlug, slugs, locale));
958
+ const byId = await repo.findManyByIds(collectionSlug, identities.filter(({ slug }) => slug === null).map(({ entry }) => entry.id));
959
+ const existing = /* @__PURE__ */ new Map();
960
+ for (const { entry, slug, locale } of identities) {
961
+ const item = slug === null ? byId.get(entry.id) : bySlug.get(locale)?.get(slug);
962
+ if (item) existing.set(seedEntryKey(entry, slug, locale), item);
963
+ }
964
+ return existing;
965
+ }
966
+ /**
967
+ * Apply hierarchical taxonomy terms (parents before children). Stops before
968
+ * creating a term once `mayCreate` returns false, and returns how many terms
969
+ * were found or created.
970
+ */
971
+ async function applyHierarchicalTerms(termRepo, taxonomyName, defLocale, terms, existingTerms, termSeedIdMap, result, onConflict, mayCreate) {
972
+ let processed = 0;
782
973
  const slugToId = /* @__PURE__ */ new Map();
783
974
  const resolveTermLocale = (term) => resolveConfiguredLocale(term.locale ?? defLocale);
784
975
  let remaining = [...terms];
@@ -792,20 +983,25 @@ async function applyHierarchicalTerms(termRepo, taxonomyName, defLocale, terms,
792
983
  if (!parentReady || !translationReady) continue;
793
984
  const parentId = term.parent ? slugToId.get(`${termLocale}::${term.parent}`) : void 0;
794
985
  const translationOf = term.translationOf ? termSeedIdMap.get(term.translationOf) : void 0;
795
- const existing = await termRepo.findBySlug(taxonomyName, term.slug, termLocale);
796
- if (existing) {
986
+ const termKey = seedTermKey(termLocale, term.slug);
987
+ const existingId = existingTerms.get(termKey);
988
+ if (existingId) {
797
989
  if (onConflict === "error") throw new Error(`Conflict: taxonomy term "${term.slug}" in "${taxonomyName}" (${termLocale}) already exists`);
798
990
  if (onConflict === "update") {
799
- await termRepo.update(existing.id, {
991
+ await termRepo.update(existingId, {
800
992
  label: term.label,
801
993
  parentId,
802
994
  data: term.description ? { description: term.description } : {}
803
995
  });
804
996
  result.taxonomies.terms++;
805
997
  }
806
- slugToId.set(`${termLocale}::${term.slug}`, existing.id);
807
- if (term.id) termSeedIdMap.set(term.id, existing.id);
998
+ slugToId.set(termKey, existingId);
999
+ if (term.id) termSeedIdMap.set(term.id, existingId);
808
1000
  } else {
1001
+ if (!mayCreate()) return {
1002
+ processed,
1003
+ complete: false
1004
+ };
809
1005
  const created = await termRepo.create({
810
1006
  name: taxonomyName,
811
1007
  slug: term.slug,
@@ -815,16 +1011,22 @@ async function applyHierarchicalTerms(termRepo, taxonomyName, defLocale, terms,
815
1011
  locale: termLocale,
816
1012
  translationOf
817
1013
  });
818
- slugToId.set(`${termLocale}::${term.slug}`, created.id);
1014
+ existingTerms.set(termKey, created.id);
1015
+ slugToId.set(termKey, created.id);
819
1016
  if (term.id) termSeedIdMap.set(term.id, created.id);
820
1017
  result.taxonomies.terms++;
821
1018
  }
1019
+ processed++;
822
1020
  processedThisPass.push(term.slug + "::" + termLocale);
823
1021
  }
824
1022
  remaining = remaining.filter((term) => !processedThisPass.includes(term.slug + "::" + resolveTermLocale(term)));
825
1023
  maxPasses--;
826
1024
  }
827
1025
  if (remaining.length > 0) console.warn(`Could not process ${remaining.length} terms due to missing parents/translations`);
1026
+ return {
1027
+ processed,
1028
+ complete: true
1029
+ };
828
1030
  }
829
1031
  /**
830
1032
  * Apply byline credits to a content entry.
@@ -1057,6 +1259,7 @@ async function resolveMedia(ref, ctx, result) {
1057
1259
  try {
1058
1260
  validateExternalUrl(url);
1059
1261
  console.log(` 📥 Downloading: ${url}`);
1262
+ ctx.budget?.countMediaDownload();
1060
1263
  const response = await ssrfSafeFetch(url, { headers: { "User-Agent": "EmDash-CMS/1.0" } });
1061
1264
  if (!response.ok) {
1062
1265
  console.warn(` ⚠️ Failed to download ${url}: ${response.status}`);
@@ -1159,5 +1362,5 @@ function getImageDimensions(buffer) {
1159
1362
  }
1160
1363
 
1161
1364
  //#endregion
1162
- export { createPublicMediaUrlResolver as a, sanitizeGalleryImages as i, apply_exports as n, resolveImageMedia as r, applySeed as t };
1163
- //# sourceMappingURL=apply-CoT603H3.mjs.map
1365
+ export { sanitizeGalleryImages as a, resolveImageMedia as i, applySeedWithinBudget as n, createPublicMediaUrlResolver as o, apply_exports as r, applySeed as t };
1366
+ //# sourceMappingURL=apply-npY1TAhG.mjs.map