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.
- package/dist/api/route-utils.mjs +6 -6
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/{api-cFFZpyS0.mjs → api-CuyNEHZG.mjs} +3 -3
- package/dist/{api-cFFZpyS0.mjs.map → api-CuyNEHZG.mjs.map} +1 -1
- package/dist/{apply-CoT603H3.mjs → apply-npY1TAhG.mjs} +237 -34
- package/dist/apply-npY1TAhG.mjs.map +1 -0
- package/dist/astro/index.d.mts +3 -3
- package/dist/astro/index.mjs +9 -9
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +3 -3
- package/dist/astro/middleware/redirect.mjs +1 -1
- package/dist/astro/middleware.d.mts +3 -3
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +19 -18
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/progress.mjs +2 -2
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +2 -2
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +11 -11
- package/dist/astro/routes/api/admin/plugins/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +11 -11
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +12 -12
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +9 -9
- package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +9 -9
- package/dist/astro/routes/api/admin/plugins/updates.mjs +8 -8
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +8 -8
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +8 -8
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/approve.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/deny.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/approvals/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/capabilities.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/advance.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/archive.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/files/_...path_.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/manifest.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/abandon.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/advance.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/analyze.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/cancel.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/execute.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/files/_...path_.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/missing.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/plan.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/receipt.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/index.mjs +3 -3
- package/dist/astro/routes/api/dashboard.mjs +2 -2
- package/dist/astro/routes/api/health.mjs +1 -1
- package/dist/astro/routes/api/import/probe.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress/media.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +3 -3
- package/dist/astro/routes/api/manifest.mjs +1 -1
- package/dist/astro/routes/api/mcp.mjs +13 -33
- package/dist/astro/routes/api/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +1 -1
- package/dist/astro/routes/api/media/_id_/upload.mjs +1 -1
- package/dist/astro/routes/api/media/_id_/usage.mjs +2 -2
- package/dist/astro/routes/api/media/_id_.mjs +2 -2
- package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs +1 -1
- package/dist/astro/routes/api/media/upload-url.mjs +1 -1
- package/dist/astro/routes/api/media.mjs +2 -2
- package/dist/astro/routes/api/redirects/404s/index.mjs +2 -2
- package/dist/astro/routes/api/redirects/404s/summary.mjs +2 -2
- package/dist/astro/routes/api/redirects/_id_.mjs +2 -2
- package/dist/astro/routes/api/redirects/index.mjs +2 -2
- package/dist/astro/routes/api/schema/block-types/_slug_/index.mjs +8 -8
- package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.mjs +8 -8
- package/dist/astro/routes/api/schema/block-types/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/reorder.mjs +8 -8
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +8 -8
- package/dist/astro/routes/api/schema/orphans/index.mjs +8 -8
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +2 -2
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +2 -2
- package/dist/astro/routes/api/settings/backups/export.mjs +2 -2
- package/dist/astro/routes/api/settings/backups/index.mjs +2 -2
- package/dist/astro/routes/api/settings.mjs +3 -3
- package/dist/astro/routes/api/setup/dev-bypass.mjs +4 -4
- package/dist/astro/routes/api/setup/index.d.mts.map +1 -1
- package/dist/astro/routes/api/setup/index.mjs +35 -10
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/snapshot.mjs +2 -2
- package/dist/astro/routes/api/snapshot.mjs.map +1 -1
- package/dist/astro/routes/robots.txt.mjs +2 -2
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +2 -2
- package/dist/astro/routes/sitemap.xml.mjs +2 -2
- package/dist/astro/types.d.mts +7 -3
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{backup-CBu0LrXf.mjs → backup-BQRj3afm.mjs} +3 -3
- package/dist/{backup-CBu0LrXf.mjs.map → backup-BQRj3afm.mjs.map} +1 -1
- package/dist/cli/index.mjs +6 -6
- package/dist/client/index.d.mts +14 -14
- package/dist/{config-5ii2haYu.d.mts → config-BjEm_3xL.d.mts} +25 -16
- package/dist/config-BjEm_3xL.d.mts.map +1 -0
- package/dist/{context-BeGccdUX.mjs → context-BV_7t-Ua.mjs} +6 -6
- package/dist/{context-BeGccdUX.mjs.map → context-BV_7t-Ua.mjs.map} +1 -1
- package/dist/{dashboard-DjrB-xxZ.mjs → dashboard-BP-iJqpd.mjs} +2 -2
- package/dist/{dashboard-DjrB-xxZ.mjs.map → dashboard-BP-iJqpd.mjs.map} +1 -1
- package/dist/db/libsql-migrations.mjs +2 -2
- package/dist/db/postgres-migrations.mjs +2 -2
- package/dist/db/sqlite-migrations.mjs +2 -2
- package/dist/{direct-executor-BmuFJJZV.mjs → direct-executor-D2lNadwo.mjs} +2 -2
- package/dist/{direct-executor-BmuFJJZV.mjs.map → direct-executor-D2lNadwo.mjs.map} +1 -1
- package/dist/{emdash-runtime-DAjghyEG.mjs → emdash-runtime-CtfnZmPi.mjs} +56 -56
- package/dist/emdash-runtime-CtfnZmPi.mjs.map +1 -0
- package/dist/{entry-lock-DVhCt49A.d.mts → entry-lock-Cf5OoIgN.d.mts} +18 -18
- package/dist/{entry-lock-DVhCt49A.d.mts.map → entry-lock-Cf5OoIgN.d.mts.map} +1 -1
- package/dist/{hreflang-DMFBL2eb.mjs → hreflang-CbVI4paR.mjs} +2 -2
- package/dist/{hreflang-DMFBL2eb.mjs.map → hreflang-CbVI4paR.mjs.map} +1 -1
- package/dist/{identity-IyNWwXr6.mjs → identity-Be5kzutJ.mjs} +2 -2
- package/dist/{identity-IyNWwXr6.mjs.map → identity-Be5kzutJ.mjs.map} +1 -1
- package/dist/{import-C3JZplWq.mjs → import-YzfKCaco.mjs} +2 -2
- package/dist/{import-C3JZplWq.mjs.map → import-YzfKCaco.mjs.map} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +16 -16
- package/dist/{manifest-builder-B8zR-73J.mjs → manifest-builder-BmSAOOEF.mjs} +2 -2
- package/dist/{manifest-builder-B8zR-73J.mjs.map → manifest-builder-BmSAOOEF.mjs.map} +1 -1
- package/dist/media/local-runtime.d.mts +3 -3
- package/dist/media/local-runtime.mjs +3 -3
- package/dist/{media-Qp5S3eH3.mjs → media-CPXTbb1R.mjs} +20 -2
- package/dist/media-CPXTbb1R.mjs.map +1 -0
- package/dist/{media-h7tOiDLj.mjs → media-JXRWDhw3.mjs} +77 -4
- package/dist/media-JXRWDhw3.mjs.map +1 -0
- package/dist/{media-usage-My-Vi-ex.mjs → media-usage-De-zeR9E.mjs} +2 -2
- package/dist/{media-usage-My-Vi-ex.mjs.map → media-usage-De-zeR9E.mjs.map} +1 -1
- package/dist/{menus-TsSGmSR4.mjs → menus-Cm68rsAq.mjs} +4 -4
- package/dist/{menus-TsSGmSR4.mjs.map → menus-Cm68rsAq.mjs.map} +1 -1
- package/dist/migrations/index.mjs +2 -2
- package/dist/page/index.mjs +2 -2
- package/dist/plugin-test-runtime.d.mts +3 -3
- package/dist/plugin-test-runtime.mjs +18 -18
- package/dist/plugin-utils.d.mts +3 -3
- package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
- package/dist/plugins/host.d.mts +1 -1
- package/dist/plugins/host.mjs +6 -6
- package/dist/{redirect-iJf5fltD.mjs → redirect-B29UjdUn.mjs} +10 -14
- package/dist/redirect-B29UjdUn.mjs.map +1 -0
- package/dist/{redirects-CYu1u1aK.mjs → redirects-Ca2MPfEG.mjs} +2 -2
- package/dist/{redirects-CYu1u1aK.mjs.map → redirects-Ca2MPfEG.mjs.map} +1 -1
- package/dist/{routes-CUsM40Cc.mjs → routes-vrR95WR6.mjs} +2 -2
- package/dist/{routes-CUsM40Cc.mjs.map → routes-vrR95WR6.mjs.map} +1 -1
- package/dist/runtime.d.mts +3 -3
- package/dist/seed/index.mjs +4 -4
- package/dist/seo/index.mjs +3 -3
- package/dist/{settings-CgjRWGW9.mjs → settings-B4vDl1fK.mjs} +2 -2
- package/dist/{settings-CgjRWGW9.mjs.map → settings-B4vDl1fK.mjs.map} +1 -1
- package/dist/{settings-2xNYr90o.mjs → settings-Dyj6v8C4.mjs} +2 -2
- package/dist/{settings-2xNYr90o.mjs.map → settings-Dyj6v8C4.mjs.map} +1 -1
- package/dist/{snapshot-CugwUqKQ.mjs → snapshot-PawoX-WG.mjs} +180 -3
- package/dist/snapshot-PawoX-WG.mjs.map +1 -0
- package/dist/{storage-query-Bdv7A62k.d.mts → storage-query-FXJH5pI8.d.mts} +6 -1
- package/dist/storage-query-FXJH5pI8.d.mts.map +1 -0
- package/dist/{transfer-BhUCrgDc.mjs → transfer-DgeCvHnj.mjs} +4 -4
- package/dist/{transfer-BhUCrgDc.mjs.map → transfer-DgeCvHnj.mjs.map} +1 -1
- package/dist/validate-CM79QXYE.d.mts.map +1 -1
- package/dist/version-DXsxRrob.mjs +7 -0
- package/dist/{version-DUQDHgrf.mjs.map → version-DXsxRrob.mjs.map} +1 -1
- package/package.json +5 -5
- package/src/api/handlers/index.ts +1 -0
- package/src/api/handlers/media-upload.ts +17 -71
- package/src/api/handlers/media.ts +97 -3
- package/src/api/handlers/snapshot.ts +257 -3
- package/src/astro/integration/vite-config.ts +10 -4
- package/src/astro/middleware.ts +1 -0
- package/src/astro/routes/api/setup/index.ts +66 -23
- package/src/astro/routes/api/snapshot.ts +2 -1
- package/src/astro/types.ts +5 -0
- package/src/cleanup.ts +10 -0
- package/src/components/Columns.astro +3 -1
- package/src/components/Cover.astro +3 -1
- package/src/database/repositories/media.ts +40 -1
- package/src/database/repositories/redirect.ts +17 -34
- package/src/emdash-runtime.ts +28 -0
- package/src/index.ts +1 -0
- package/src/mcp/server.ts +13 -48
- package/src/seed/apply.ts +322 -41
- package/dist/apply-CoT603H3.mjs.map +0 -1
- package/dist/config-5ii2haYu.d.mts.map +0 -1
- package/dist/emdash-runtime-DAjghyEG.mjs.map +0 -1
- package/dist/media-Qp5S3eH3.mjs.map +0 -1
- package/dist/media-h7tOiDLj.mjs.map +0 -1
- package/dist/redirect-iJf5fltD.mjs.map +0 -1
- package/dist/snapshot-CugwUqKQ.mjs.map +0 -1
- package/dist/storage-query-Bdv7A62k.d.mts.map +0 -1
- package/dist/version-DUQDHgrf.mjs +0 -7
package/src/seed/apply.ts
CHANGED
|
@@ -6,7 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { imageSize } from "image-size";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
Kysely,
|
|
11
|
+
KyselyPlugin,
|
|
12
|
+
PluginTransformQueryArgs,
|
|
13
|
+
PluginTransformResultArgs,
|
|
14
|
+
QueryResult,
|
|
15
|
+
RootOperationNode,
|
|
16
|
+
UnknownRow,
|
|
17
|
+
} from "kysely";
|
|
10
18
|
import mime from "mime/lite";
|
|
11
19
|
import { ulid } from "ulidx";
|
|
12
20
|
|
|
@@ -23,6 +31,7 @@ import {
|
|
|
23
31
|
saveTaxonomyStructure,
|
|
24
32
|
} from "../database/repositories/taxonomy-def.js";
|
|
25
33
|
import { TaxonomyRepository } from "../database/repositories/taxonomy.js";
|
|
34
|
+
import type { ContentItem } from "../database/repositories/types.js";
|
|
26
35
|
import { withTransaction } from "../database/transaction.js";
|
|
27
36
|
import type { Database } from "../database/types.js";
|
|
28
37
|
import type { MediaValue } from "../fields/types.js";
|
|
@@ -37,10 +46,12 @@ import { FTSManager } from "../search/fts-manager.js";
|
|
|
37
46
|
import { invalidateSiteSettingsCache, setSiteSettings } from "../settings/index.js";
|
|
38
47
|
import type { SiteSettings } from "../settings/types.js";
|
|
39
48
|
import type { Storage } from "../storage/types.js";
|
|
49
|
+
import { chunks, SQL_BATCH_SIZE } from "../utils/chunks.js";
|
|
40
50
|
import type {
|
|
41
51
|
SeedFile,
|
|
42
52
|
SeedApplyOptions,
|
|
43
53
|
SeedApplyResult,
|
|
54
|
+
SeedContentEntry,
|
|
44
55
|
SeedCollection,
|
|
45
56
|
SeedTaxonomy,
|
|
46
57
|
SeedTaxonomyTerm,
|
|
@@ -158,14 +169,90 @@ export async function applySeed(
|
|
|
158
169
|
seed: SeedFile,
|
|
159
170
|
options: SeedApplyOptions = {},
|
|
160
171
|
): Promise<SeedApplyResult> {
|
|
161
|
-
|
|
172
|
+
const { result } = await coalesceObjectCacheWrites(() =>
|
|
173
|
+
applySeedWrites(db, seed, options, null),
|
|
174
|
+
);
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Per-call limits for `applySeedWithinBudget`. A limit that is left out is not checked. */
|
|
179
|
+
export interface SeedApplyBudget {
|
|
180
|
+
queries?: number;
|
|
181
|
+
mediaDownloads?: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** How many of a seed's items (taxonomy terms, bylines, content entries) exist after a call. */
|
|
185
|
+
export interface SeedApplyProgress {
|
|
186
|
+
done: number;
|
|
187
|
+
total: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Apply a seed over several calls, for a platform that caps the queries or
|
|
192
|
+
* fetches of one request.
|
|
193
|
+
*
|
|
194
|
+
* The limits are thresholds, checked before each taxonomy term, byline and
|
|
195
|
+
* content entry is created and before the phases that follow content. Schema,
|
|
196
|
+
* taxonomy definitions and settings are applied in every call, the item that
|
|
197
|
+
* crosses a threshold finishes, and the phases after content (menus,
|
|
198
|
+
* redirects, widgets, sections) run in one call, so set the limits well below
|
|
199
|
+
* the platform's. A call that stops returns `complete: false`; applying the
|
|
200
|
+
* same seed again continues with the first item not yet created. A call
|
|
201
|
+
* creates at least one item before it stops, so repeated calls finish. A
|
|
202
|
+
* `$media` URL used by entries in different calls is downloaded and stored
|
|
203
|
+
* again, as a new media row, in each of those calls. Requires
|
|
204
|
+
* `onConflict: "skip"`.
|
|
205
|
+
*/
|
|
206
|
+
export async function applySeedWithinBudget(
|
|
207
|
+
db: Kysely<Database>,
|
|
208
|
+
seed: SeedFile,
|
|
209
|
+
options: SeedApplyOptions,
|
|
210
|
+
limits: SeedApplyBudget,
|
|
211
|
+
): Promise<{ result: SeedApplyResult; complete: boolean; progress: SeedApplyProgress }> {
|
|
212
|
+
const budget = new SeedBudget(limits);
|
|
213
|
+
return coalesceObjectCacheWrites(() =>
|
|
214
|
+
applySeedWrites(db.withPlugin(budget), seed, options, budget),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Counts what a budgeted call spends. As a Kysely plugin it sees every query
|
|
220
|
+
* the call runs, including those inside transactions.
|
|
221
|
+
*/
|
|
222
|
+
class SeedBudget implements KyselyPlugin {
|
|
223
|
+
#queries = 0;
|
|
224
|
+
#mediaDownloads = 0;
|
|
225
|
+
|
|
226
|
+
constructor(private readonly limits: SeedApplyBudget) {}
|
|
227
|
+
|
|
228
|
+
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
|
|
229
|
+
this.#queries++;
|
|
230
|
+
return args.node;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
|
|
234
|
+
return Promise.resolve(args.result);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
countMediaDownload(): void {
|
|
238
|
+
this.#mediaDownloads++;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
isSpent(): boolean {
|
|
242
|
+
const { queries, mediaDownloads } = this.limits;
|
|
243
|
+
return (
|
|
244
|
+
(queries !== undefined && this.#queries >= queries) ||
|
|
245
|
+
(mediaDownloads !== undefined && this.#mediaDownloads >= mediaDownloads)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
162
248
|
}
|
|
163
249
|
|
|
164
250
|
async function applySeedWrites(
|
|
165
251
|
db: Kysely<Database>,
|
|
166
252
|
seed: SeedFile,
|
|
167
253
|
options: SeedApplyOptions,
|
|
168
|
-
|
|
254
|
+
budget: SeedBudget | null,
|
|
255
|
+
): Promise<{ result: SeedApplyResult; complete: boolean; progress: SeedApplyProgress }> {
|
|
169
256
|
// Validate seed first
|
|
170
257
|
const validation = validateSeed(seed);
|
|
171
258
|
if (!validation.valid) {
|
|
@@ -179,6 +266,10 @@ async function applySeedWrites(
|
|
|
179
266
|
onConflict = "skip",
|
|
180
267
|
} = options;
|
|
181
268
|
|
|
269
|
+
if (budget && onConflict !== "skip") {
|
|
270
|
+
throw new Error('A seed budget requires onConflict: "skip"');
|
|
271
|
+
}
|
|
272
|
+
|
|
182
273
|
// Result counters
|
|
183
274
|
const result: SeedApplyResult = {
|
|
184
275
|
blockTypes: { created: 0, skipped: 0, updated: 0 },
|
|
@@ -194,6 +285,13 @@ async function applySeedWrites(
|
|
|
194
285
|
content: { created: 0, skipped: 0, updated: 0 },
|
|
195
286
|
media: { created: 0, skipped: 0 },
|
|
196
287
|
};
|
|
288
|
+
let complete = true;
|
|
289
|
+
const progress: SeedApplyProgress = { done: 0, total: countSeedItems(seed, includeContent) };
|
|
290
|
+
// With a budget, `onConflict` is "skip", so these counters count creations only.
|
|
291
|
+
const mayCreateItem = (): boolean =>
|
|
292
|
+
!budget ||
|
|
293
|
+
result.taxonomies.terms + result.bylines.created + result.content.created === 0 ||
|
|
294
|
+
!budget.isSpent();
|
|
197
295
|
|
|
198
296
|
// Media context for $media resolution
|
|
199
297
|
const mediaContext: MediaContext = {
|
|
@@ -201,6 +299,7 @@ async function applySeedWrites(
|
|
|
201
299
|
storage: storage ?? null,
|
|
202
300
|
skipMediaDownload,
|
|
203
301
|
mediaCache: new Map(), // Cache downloaded media by URL to avoid re-downloading
|
|
302
|
+
budget,
|
|
204
303
|
};
|
|
205
304
|
|
|
206
305
|
// Apply order (critical for foreign keys and references):
|
|
@@ -482,36 +581,47 @@ async function applySeedWrites(
|
|
|
482
581
|
// Create terms (if provided)
|
|
483
582
|
if (includeContent && taxonomy.terms && taxonomy.terms.length > 0) {
|
|
484
583
|
const termRepo = new TaxonomyRepository(db);
|
|
584
|
+
const existingTerms = await findExistingSeedTerms(db, taxonomy.name, taxonomy.terms);
|
|
485
585
|
|
|
486
586
|
if (structure.hierarchical) {
|
|
487
|
-
await applyHierarchicalTerms(
|
|
587
|
+
const applied = await applyHierarchicalTerms(
|
|
488
588
|
termRepo,
|
|
489
589
|
taxonomy.name,
|
|
490
590
|
defLocale,
|
|
491
591
|
taxonomy.terms,
|
|
592
|
+
existingTerms,
|
|
492
593
|
termSeedIdMap,
|
|
493
594
|
result,
|
|
494
595
|
onConflict,
|
|
596
|
+
mayCreateItem,
|
|
495
597
|
);
|
|
598
|
+
progress.done += applied.processed;
|
|
599
|
+
complete = applied.complete;
|
|
496
600
|
} else {
|
|
497
601
|
for (const term of taxonomy.terms) {
|
|
498
602
|
const termLocale = resolveConfiguredLocale(term.locale ?? defLocale);
|
|
499
|
-
const
|
|
500
|
-
|
|
603
|
+
const termKey = seedTermKey(termLocale, term.slug);
|
|
604
|
+
const existingId = existingTerms.get(termKey);
|
|
605
|
+
if (existingId) {
|
|
501
606
|
if (onConflict === "error") {
|
|
502
607
|
throw new Error(
|
|
503
608
|
`Conflict: taxonomy term "${term.slug}" in "${taxonomy.name}" (${termLocale}) already exists`,
|
|
504
609
|
);
|
|
505
610
|
}
|
|
506
611
|
if (onConflict === "update") {
|
|
507
|
-
await termRepo.update(
|
|
612
|
+
await termRepo.update(existingId, {
|
|
508
613
|
label: term.label,
|
|
509
614
|
data: term.description ? { description: term.description } : {},
|
|
510
615
|
});
|
|
511
616
|
result.taxonomies.terms++;
|
|
512
617
|
}
|
|
513
|
-
if (term.id) termSeedIdMap.set(term.id,
|
|
618
|
+
if (term.id) termSeedIdMap.set(term.id, existingId);
|
|
619
|
+
progress.done++;
|
|
514
620
|
} else {
|
|
621
|
+
if (!mayCreateItem()) {
|
|
622
|
+
complete = false;
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
515
625
|
const translationOf = term.translationOf
|
|
516
626
|
? termSeedIdMap.get(term.translationOf)
|
|
517
627
|
: undefined;
|
|
@@ -523,12 +633,15 @@ async function applySeedWrites(
|
|
|
523
633
|
locale: termLocale,
|
|
524
634
|
translationOf,
|
|
525
635
|
});
|
|
636
|
+
existingTerms.set(termKey, created.id);
|
|
526
637
|
if (term.id) termSeedIdMap.set(term.id, created.id);
|
|
527
638
|
result.taxonomies.terms++;
|
|
639
|
+
progress.done++;
|
|
528
640
|
}
|
|
529
641
|
}
|
|
530
642
|
}
|
|
531
643
|
}
|
|
644
|
+
if (!complete) break;
|
|
532
645
|
}
|
|
533
646
|
|
|
534
647
|
// Seeded/updated defs change which taxonomies exist — clear the
|
|
@@ -539,11 +652,15 @@ async function applySeedWrites(
|
|
|
539
652
|
}
|
|
540
653
|
|
|
541
654
|
// 6. Bylines
|
|
542
|
-
if (includeContent && seed.bylines) {
|
|
655
|
+
if (complete && includeContent && seed.bylines) {
|
|
543
656
|
const bylineRepo = new BylineRepository(db);
|
|
657
|
+
const existingBylines = await findExistingSeedBylines(
|
|
658
|
+
db,
|
|
659
|
+
seed.bylines.map((byline) => byline.slug),
|
|
660
|
+
);
|
|
544
661
|
for (const byline of seed.bylines) {
|
|
545
|
-
const
|
|
546
|
-
if (
|
|
662
|
+
const existingId = existingBylines.get(byline.slug);
|
|
663
|
+
if (existingId) {
|
|
547
664
|
if (onConflict === "error") {
|
|
548
665
|
throw new Error(`Conflict: byline "${byline.slug}" already exists`);
|
|
549
666
|
}
|
|
@@ -555,7 +672,7 @@ async function applySeedWrites(
|
|
|
555
672
|
// one untouched.
|
|
556
673
|
const avatar = byline.avatar ? await resolveSeedBylineAvatar(db, byline.avatar) : null;
|
|
557
674
|
try {
|
|
558
|
-
const updated = await bylineRepo.update(
|
|
675
|
+
const updated = await bylineRepo.update(existingId, {
|
|
559
676
|
displayName: byline.displayName,
|
|
560
677
|
bio: byline.bio ?? null,
|
|
561
678
|
websiteUrl: byline.websiteUrl ?? null,
|
|
@@ -563,7 +680,7 @@ async function applySeedWrites(
|
|
|
563
680
|
...(avatar ? { avatarMediaId: avatar.id } : {}),
|
|
564
681
|
});
|
|
565
682
|
// update() returns null (no throw) if the row vanished between
|
|
566
|
-
//
|
|
683
|
+
// the lookup and here; treat that as a failure so the catch
|
|
567
684
|
// cleans up any freshly-created avatar media instead of leaking it.
|
|
568
685
|
if (!updated) {
|
|
569
686
|
throw new Error(`Byline "${byline.slug}" disappeared during update`);
|
|
@@ -574,18 +691,24 @@ async function applySeedWrites(
|
|
|
574
691
|
if (avatar?.created) await deleteMediaRow(db, avatar.id);
|
|
575
692
|
throw error;
|
|
576
693
|
}
|
|
577
|
-
seedBylineIdMap.set(byline.id,
|
|
694
|
+
seedBylineIdMap.set(byline.id, existingId);
|
|
578
695
|
result.bylines.updated++;
|
|
579
696
|
if (avatar?.created) result.media.created++;
|
|
697
|
+
progress.done++;
|
|
580
698
|
continue;
|
|
581
699
|
}
|
|
582
700
|
|
|
583
701
|
// skip
|
|
584
|
-
seedBylineIdMap.set(byline.id,
|
|
702
|
+
seedBylineIdMap.set(byline.id, existingId);
|
|
585
703
|
result.bylines.skipped++;
|
|
704
|
+
progress.done++;
|
|
586
705
|
continue;
|
|
587
706
|
}
|
|
588
707
|
|
|
708
|
+
if (!mayCreateItem()) {
|
|
709
|
+
complete = false;
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
589
712
|
const avatar = byline.avatar ? await resolveSeedBylineAvatar(db, byline.avatar) : null;
|
|
590
713
|
let createdId: string;
|
|
591
714
|
try {
|
|
@@ -602,14 +725,16 @@ async function applySeedWrites(
|
|
|
602
725
|
if (avatar?.created) await deleteMediaRow(db, avatar.id);
|
|
603
726
|
throw error;
|
|
604
727
|
}
|
|
728
|
+
existingBylines.set(byline.slug, createdId);
|
|
605
729
|
seedBylineIdMap.set(byline.id, createdId);
|
|
606
730
|
result.bylines.created++;
|
|
607
731
|
if (avatar?.created) result.media.created++;
|
|
732
|
+
progress.done++;
|
|
608
733
|
}
|
|
609
734
|
}
|
|
610
735
|
|
|
611
736
|
// 7. Content (created before menus so refs can resolve)
|
|
612
|
-
if (includeContent && seed.content) {
|
|
737
|
+
if (complete && includeContent && seed.content) {
|
|
613
738
|
const contentRepo = new ContentRepository(db);
|
|
614
739
|
const schemaRegistry = new SchemaRegistry(db);
|
|
615
740
|
// Settings and fields are all written above, so every entry can share the
|
|
@@ -624,19 +749,16 @@ async function applySeedWrites(
|
|
|
624
749
|
const resolvedBlockTypes = collectionInfo?.fields.some((field) => field.type === "blocks")
|
|
625
750
|
? await resolveBlockTypes(db)
|
|
626
751
|
: undefined;
|
|
752
|
+
const existingEntries = await findExistingSeedEntries(
|
|
753
|
+
contentRepo,
|
|
754
|
+
collectionSlug,
|
|
755
|
+
entries,
|
|
756
|
+
defaultLocale,
|
|
757
|
+
);
|
|
627
758
|
for (const entry of entries) {
|
|
628
|
-
const entrySlug =
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
// export (which omits `locale`) is filed under the project default
|
|
632
|
-
// rather than `en` (#1421).
|
|
633
|
-
const entryLocale = resolveConfiguredLocale(entry.locale ?? defaultLocale);
|
|
634
|
-
|
|
635
|
-
// Slugful entries use the existing locale-aware key. Slugless seed
|
|
636
|
-
// entries persist their seed ID, which keeps re-application idempotent.
|
|
637
|
-
const existing = entrySlug
|
|
638
|
-
? await contentRepo.findBySlug(collectionSlug, entrySlug, entryLocale)
|
|
639
|
-
: await contentRepo.findById(collectionSlug, entry.id);
|
|
759
|
+
const { slug: entrySlug, locale: entryLocale } = seedEntryIdentity(entry, defaultLocale);
|
|
760
|
+
const entryKey = seedEntryKey(entry, entrySlug, entryLocale);
|
|
761
|
+
const existing = existingEntries.get(entryKey);
|
|
640
762
|
|
|
641
763
|
if (existing) {
|
|
642
764
|
if (onConflict === "error") {
|
|
@@ -738,6 +860,7 @@ async function applySeedWrites(
|
|
|
738
860
|
|
|
739
861
|
seedIdMap.set(entry.id, existing.id);
|
|
740
862
|
result.content.updated++;
|
|
863
|
+
progress.done++;
|
|
741
864
|
await markSeedContentCollectionStale(collectionSlug);
|
|
742
865
|
continue;
|
|
743
866
|
}
|
|
@@ -745,9 +868,15 @@ async function applySeedWrites(
|
|
|
745
868
|
// skip
|
|
746
869
|
result.content.skipped++;
|
|
747
870
|
seedIdMap.set(entry.id, existing.id);
|
|
871
|
+
progress.done++;
|
|
748
872
|
continue;
|
|
749
873
|
}
|
|
750
874
|
|
|
875
|
+
if (!mayCreateItem()) {
|
|
876
|
+
complete = false;
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
|
|
751
880
|
// Resolve $ref and $media in data
|
|
752
881
|
let resolvedData = await resolveReferences(entry.data, seedIdMap, mediaContext, result);
|
|
753
882
|
if (collectionInfo) {
|
|
@@ -828,15 +957,26 @@ async function applySeedWrites(
|
|
|
828
957
|
}
|
|
829
958
|
|
|
830
959
|
seedIdMap.set(entry.id, created.id);
|
|
960
|
+
existingEntries.set(entryKey, created);
|
|
831
961
|
result.content.created++;
|
|
962
|
+
progress.done++;
|
|
832
963
|
await markSeedContentCollectionStale(collectionSlug);
|
|
833
964
|
}
|
|
965
|
+
if (!complete) break;
|
|
834
966
|
}
|
|
835
967
|
} finally {
|
|
836
968
|
await retryFailedSeedContentStaleMarks();
|
|
837
969
|
}
|
|
838
970
|
}
|
|
839
971
|
|
|
972
|
+
if (complete && !mayCreateItem()) {
|
|
973
|
+
complete = false;
|
|
974
|
+
}
|
|
975
|
+
if (!complete) {
|
|
976
|
+
await invalidateSeedCaches();
|
|
977
|
+
return { result, complete, progress };
|
|
978
|
+
}
|
|
979
|
+
|
|
840
980
|
// 8. Menus and Menu Items (after content so refs can resolve)
|
|
841
981
|
if (seed.menus) {
|
|
842
982
|
// seed-local id -> resolved info, used to wire `translationOf` refs.
|
|
@@ -1064,31 +1204,165 @@ async function applySeedWrites(
|
|
|
1064
1204
|
}
|
|
1065
1205
|
}
|
|
1066
1206
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1207
|
+
await invalidateSeedCaches();
|
|
1208
|
+
|
|
1209
|
+
return { result, complete, progress };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function countSeedItems(seed: SeedFile, includeContent: boolean): number {
|
|
1213
|
+
if (!includeContent) return 0;
|
|
1214
|
+
const terms = (seed.taxonomies ?? []).reduce(
|
|
1215
|
+
(total, taxonomy) => total + (taxonomy.terms?.length ?? 0),
|
|
1216
|
+
0,
|
|
1217
|
+
);
|
|
1218
|
+
const entries = Object.values(seed.content ?? {}).reduce(
|
|
1219
|
+
(total, collectionEntries) => total + collectionEntries.length,
|
|
1220
|
+
0,
|
|
1221
|
+
);
|
|
1222
|
+
return terms + (seed.bylines?.length ?? 0) + entries;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function seedTermKey(locale: string, slug: string): string {
|
|
1226
|
+
return `${locale}::${slug}`;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Find which of a taxonomy's seed terms already exist, as term ids keyed by
|
|
1231
|
+
* `seedTermKey`. Each query covers a batch of slugs.
|
|
1232
|
+
*/
|
|
1233
|
+
async function findExistingSeedTerms(
|
|
1234
|
+
db: Kysely<Database>,
|
|
1235
|
+
taxonomyName: string,
|
|
1236
|
+
terms: SeedTaxonomyTerm[],
|
|
1237
|
+
): Promise<Map<string, string>> {
|
|
1238
|
+
const existing = new Map<string, string>();
|
|
1239
|
+
for (const batch of chunks([...new Set(terms.map((term) => term.slug))], SQL_BATCH_SIZE)) {
|
|
1240
|
+
const rows = await db
|
|
1241
|
+
.selectFrom("taxonomies")
|
|
1242
|
+
.select(["id", "slug", "locale"])
|
|
1243
|
+
.where("name", "=", taxonomyName)
|
|
1244
|
+
.where("slug", "in", batch)
|
|
1245
|
+
.execute();
|
|
1246
|
+
for (const row of rows) existing.set(seedTermKey(row.locale, row.slug), row.id);
|
|
1247
|
+
}
|
|
1248
|
+
return existing;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Find which seed bylines already exist, as byline ids keyed by slug. A slug
|
|
1253
|
+
* in several locales resolves to the lowest locale code, as
|
|
1254
|
+
* `BylineRepository.findBySlug` does. Each query covers a batch of slugs.
|
|
1255
|
+
*/
|
|
1256
|
+
async function findExistingSeedBylines(
|
|
1257
|
+
db: Kysely<Database>,
|
|
1258
|
+
slugs: string[],
|
|
1259
|
+
): Promise<Map<string, string>> {
|
|
1260
|
+
const existing = new Map<string, string>();
|
|
1261
|
+
for (const batch of chunks([...new Set(slugs)], SQL_BATCH_SIZE)) {
|
|
1262
|
+
const rows = await db
|
|
1263
|
+
.selectFrom("_emdash_bylines")
|
|
1264
|
+
.select(["id", "slug"])
|
|
1265
|
+
.where("slug", "in", batch)
|
|
1266
|
+
.orderBy("locale", "asc")
|
|
1267
|
+
.execute();
|
|
1268
|
+
for (const row of rows) {
|
|
1269
|
+
if (!existing.has(row.slug)) existing.set(row.slug, row.id);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return existing;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Invalidate caches that may have been affected by seed data.
|
|
1277
|
+
* Seed creates bylines, redirects, and collections, all of which
|
|
1278
|
+
* have module-level caches in the hot path.
|
|
1279
|
+
*/
|
|
1280
|
+
async function invalidateSeedCaches(): Promise<void> {
|
|
1070
1281
|
const { invalidateBylineCache } = await import("../bylines/index.js");
|
|
1071
1282
|
const { invalidateRedirectCache } = await import("../redirects/cache.js");
|
|
1072
1283
|
const { invalidateUrlPatternCache } = await import("../query.js");
|
|
1073
1284
|
invalidateBylineCache();
|
|
1074
1285
|
invalidateRedirectCache();
|
|
1075
1286
|
invalidateUrlPatternCache();
|
|
1287
|
+
}
|
|
1076
1288
|
|
|
1077
|
-
|
|
1289
|
+
function seedEntryIdentity(
|
|
1290
|
+
entry: SeedContentEntry,
|
|
1291
|
+
defaultLocale: string,
|
|
1292
|
+
): { slug: string | null; locale: string } {
|
|
1293
|
+
return {
|
|
1294
|
+
slug: typeof entry.slug === "string" && entry.slug.trim().length > 0 ? entry.slug : null,
|
|
1295
|
+
// Resolve the entry's locale up front so a non-`en` single-locale
|
|
1296
|
+
// export (which omits `locale`) is filed under the project default
|
|
1297
|
+
// rather than `en`.
|
|
1298
|
+
locale: resolveConfiguredLocale(entry.locale ?? defaultLocale),
|
|
1299
|
+
};
|
|
1078
1300
|
}
|
|
1079
1301
|
|
|
1080
1302
|
/**
|
|
1081
|
-
*
|
|
1303
|
+
* Slugful entries use the existing locale-aware key. Slugless seed entries
|
|
1304
|
+
* persist their seed ID, which keeps re-application idempotent.
|
|
1305
|
+
*/
|
|
1306
|
+
function seedEntryKey(entry: SeedContentEntry, slug: string | null, locale: string): string {
|
|
1307
|
+
return slug === null ? `id:${entry.id}` : `slug:${locale}:${slug}`;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Find which of a collection's seed entries already exist, keyed by
|
|
1312
|
+
* `seedEntryKey`. Each query covers a batch of entries.
|
|
1313
|
+
*/
|
|
1314
|
+
async function findExistingSeedEntries(
|
|
1315
|
+
repo: ContentRepository,
|
|
1316
|
+
collectionSlug: string,
|
|
1317
|
+
entries: SeedContentEntry[],
|
|
1318
|
+
defaultLocale: string,
|
|
1319
|
+
): Promise<Map<string, ContentItem>> {
|
|
1320
|
+
const identities = entries.map((entry) => ({
|
|
1321
|
+
entry,
|
|
1322
|
+
...seedEntryIdentity(entry, defaultLocale),
|
|
1323
|
+
}));
|
|
1324
|
+
const slugsByLocale = new Map<string, string[]>();
|
|
1325
|
+
for (const { slug, locale } of identities) {
|
|
1326
|
+
if (slug === null) continue;
|
|
1327
|
+
const slugs = slugsByLocale.get(locale);
|
|
1328
|
+
if (slugs) slugs.push(slug);
|
|
1329
|
+
else slugsByLocale.set(locale, [slug]);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const bySlug = new Map<string, Map<string, ContentItem>>();
|
|
1333
|
+
for (const [locale, slugs] of slugsByLocale) {
|
|
1334
|
+
bySlug.set(locale, await repo.findManyBySlugsInLocale(collectionSlug, slugs, locale));
|
|
1335
|
+
}
|
|
1336
|
+
const byId = await repo.findManyByIds(
|
|
1337
|
+
collectionSlug,
|
|
1338
|
+
identities.filter(({ slug }) => slug === null).map(({ entry }) => entry.id),
|
|
1339
|
+
);
|
|
1340
|
+
|
|
1341
|
+
const existing = new Map<string, ContentItem>();
|
|
1342
|
+
for (const { entry, slug, locale } of identities) {
|
|
1343
|
+
const item = slug === null ? byId.get(entry.id) : bySlug.get(locale)?.get(slug);
|
|
1344
|
+
if (item) existing.set(seedEntryKey(entry, slug, locale), item);
|
|
1345
|
+
}
|
|
1346
|
+
return existing;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Apply hierarchical taxonomy terms (parents before children). Stops before
|
|
1351
|
+
* creating a term once `mayCreate` returns false, and returns how many terms
|
|
1352
|
+
* were found or created.
|
|
1082
1353
|
*/
|
|
1083
1354
|
async function applyHierarchicalTerms(
|
|
1084
1355
|
termRepo: TaxonomyRepository,
|
|
1085
1356
|
taxonomyName: string,
|
|
1086
1357
|
defLocale: string,
|
|
1087
1358
|
terms: SeedTaxonomyTerm[],
|
|
1359
|
+
existingTerms: Map<string, string>,
|
|
1088
1360
|
termSeedIdMap: Map<string, string>,
|
|
1089
1361
|
result: SeedApplyResult,
|
|
1090
|
-
onConflict: "skip" | "update" | "error"
|
|
1091
|
-
|
|
1362
|
+
onConflict: "skip" | "update" | "error",
|
|
1363
|
+
mayCreate: () => boolean,
|
|
1364
|
+
): Promise<{ processed: number; complete: boolean }> {
|
|
1365
|
+
let processed = 0;
|
|
1092
1366
|
// "locale::slug" -> id, so the same slug can resolve per locale.
|
|
1093
1367
|
const slugToId = new Map<string, string>();
|
|
1094
1368
|
const resolveTermLocale = (term: SeedTaxonomyTerm) =>
|
|
@@ -1111,24 +1385,26 @@ async function applyHierarchicalTerms(
|
|
|
1111
1385
|
const parentId = term.parent ? slugToId.get(`${termLocale}::${term.parent}`) : undefined;
|
|
1112
1386
|
const translationOf = term.translationOf ? termSeedIdMap.get(term.translationOf) : undefined;
|
|
1113
1387
|
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1388
|
+
const termKey = seedTermKey(termLocale, term.slug);
|
|
1389
|
+
const existingId = existingTerms.get(termKey);
|
|
1390
|
+
if (existingId) {
|
|
1116
1391
|
if (onConflict === "error") {
|
|
1117
1392
|
throw new Error(
|
|
1118
1393
|
`Conflict: taxonomy term "${term.slug}" in "${taxonomyName}" (${termLocale}) already exists`,
|
|
1119
1394
|
);
|
|
1120
1395
|
}
|
|
1121
1396
|
if (onConflict === "update") {
|
|
1122
|
-
await termRepo.update(
|
|
1397
|
+
await termRepo.update(existingId, {
|
|
1123
1398
|
label: term.label,
|
|
1124
1399
|
parentId,
|
|
1125
1400
|
data: term.description ? { description: term.description } : {},
|
|
1126
1401
|
});
|
|
1127
1402
|
result.taxonomies.terms++;
|
|
1128
1403
|
}
|
|
1129
|
-
slugToId.set(
|
|
1130
|
-
if (term.id) termSeedIdMap.set(term.id,
|
|
1404
|
+
slugToId.set(termKey, existingId);
|
|
1405
|
+
if (term.id) termSeedIdMap.set(term.id, existingId);
|
|
1131
1406
|
} else {
|
|
1407
|
+
if (!mayCreate()) return { processed, complete: false };
|
|
1132
1408
|
const created = await termRepo.create({
|
|
1133
1409
|
name: taxonomyName,
|
|
1134
1410
|
slug: term.slug,
|
|
@@ -1138,10 +1414,12 @@ async function applyHierarchicalTerms(
|
|
|
1138
1414
|
locale: termLocale,
|
|
1139
1415
|
translationOf,
|
|
1140
1416
|
});
|
|
1141
|
-
|
|
1417
|
+
existingTerms.set(termKey, created.id);
|
|
1418
|
+
slugToId.set(termKey, created.id);
|
|
1142
1419
|
if (term.id) termSeedIdMap.set(term.id, created.id);
|
|
1143
1420
|
result.taxonomies.terms++;
|
|
1144
1421
|
}
|
|
1422
|
+
processed++;
|
|
1145
1423
|
|
|
1146
1424
|
processedThisPass.push(term.slug + "::" + termLocale);
|
|
1147
1425
|
}
|
|
@@ -1155,6 +1433,7 @@ async function applyHierarchicalTerms(
|
|
|
1155
1433
|
if (remaining.length > 0) {
|
|
1156
1434
|
console.warn(`Could not process ${remaining.length} terms due to missing parents/translations`);
|
|
1157
1435
|
}
|
|
1436
|
+
return { processed, complete: true };
|
|
1158
1437
|
}
|
|
1159
1438
|
|
|
1160
1439
|
/**
|
|
@@ -1377,6 +1656,7 @@ interface MediaContext {
|
|
|
1377
1656
|
storage: Storage | null;
|
|
1378
1657
|
skipMediaDownload: boolean;
|
|
1379
1658
|
mediaCache: Map<string, MediaValue>; // URL -> resolved MediaValue
|
|
1659
|
+
budget: SeedBudget | null;
|
|
1380
1660
|
}
|
|
1381
1661
|
|
|
1382
1662
|
/**
|
|
@@ -1563,6 +1843,7 @@ async function resolveMedia(
|
|
|
1563
1843
|
|
|
1564
1844
|
// Download the media (ssrfSafeFetch re-validates redirect targets)
|
|
1565
1845
|
console.log(` 📥 Downloading: ${url}`);
|
|
1846
|
+
ctx.budget?.countMediaDownload();
|
|
1566
1847
|
const response = await ssrfSafeFetch(url, {
|
|
1567
1848
|
headers: {
|
|
1568
1849
|
// Some services like Unsplash require a user-agent
|