mirror of
https://github.com/strapi/strapi.git
synced 2026-05-03 16:22:30 +00:00
19fef31e08
* chore(examples): add mariadb + sqlite + podman support to complex Extends the complex example's DB tooling to cover all Strapi-supported database dialects and both container runtimes, as groundwork for a migration performance benchmark harness: - New compose.js runtime shim auto-detects podman compose / podman-compose / docker compose / docker-compose and the matching container CLI; all existing db-* scripts now go through it so podman-only environments work without installing docker - New db-mariadb.js mirrors db-mysql.js using mariadb-dump / mariadb CLIs and adds a mariadb:11 service on port 3307 to docker-compose.dev.yml - New db-sqlite.js handles file-based snapshot/restore/wipe/check via fs.copy / better-sqlite3 - db-utils.js falls back to `<runtime> ps --filter name=` for container lookup since podman-compose doesn't support `ps -q` - develop-with-db.js and the v4 templates (develop-with-db.js, seed-with-db.js) handle mariadb + sqlite (sqlite skips compose) - setup-v4-project.js includes better-sqlite3 in v4 deps, database.js template covers all 4 clients, and compose.js is copied into the v4 scaffold scripts dir (dep of db-utils.js) All four DBs smoke-tested locally against podman: start/check/snapshot/ restore/wipe cycle works for mariadb; cp-based snapshot cycle works for sqlite. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(examples): add migration perf benchmark harness Three new scripts enable per-migration timing and baseline-vs-candidate comparison reports for v4→v5 migrations in the complex example: - bench-hook.js: Node --require preload that intercepts require('umzug') and subscribes to Umzug's native `migrating`/`migrated` events for sub-ms timing. Captures every migration that runs (including dynamically registered ones like discard-drafts and EE-only release migrations) without hardcoding names. Dumps to a JSON file on process exit; self- disables when STRAPI_BENCH_HOOK_OUTPUT is unset. - bench.js: orchestrator with `run`, `seed`, and `suite` subcommands. `run` restores a snapshot, spawns Strapi in migrate-then-exit mode with the hook preload, collects row counts, and writes a result JSON with baseline/candidate attribution, env capture (node, CPU, memory, DB version, host type), and config (multiplier, seed/hook modes). `seed` wipes the DB, runs the v4 seed via seed-with-db.js, then snapshots. First iteration supports --strapi-source=local only; experimental/pinned are stubbed with a clear error. - bench-compare.js: takes N labels and emits both a clipboard-friendly markdown report (stdout + results/compare-*.md) and a self-contained HTML report (results/compare-*.html) with inline SVG bar charts, per-DB grid, sortable tables, collapsible raw JSON, and a light/dark adaptive theme via prefers-color-scheme. No CDN deps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(examples): bench harness smoke-test fixes Fixes discovered during the end-to-end smoke test on the existing 6 content types at multiplier=1: - bench-hook.js: switch from subclass-based wrapping to in-place Umzug.prototype.up patching. The subclass approach replaced the module export at require time, but Node's module cache hands out the original class on subsequent requires, so listeners weren't attached on all instances. In-place prototype patching works for every instance regardless of how Umzug was imported. - bench-hook.js: flush incrementally after each recorded migration. Strapi's shutdown path can bypass process.on('exit') handlers under some conditions (signal or explicit exit from deep inside), causing fully-collected timing data to be lost. Writing after each recording makes the benchmark resilient to any exit path. - bench.js: compile TypeScript configs via @strapi/typescript-utils before createStrapi().load(). The examples/complex project has .ts config files; the Strapi CLI compiles them to dist/ before boot but our direct node -e loader skipped this, producing "db.config.connection undefined" failures. - bench.js: propagate STRAPI_BENCH_HOOK_DEBUG to the Strapi child so debug output is visible when tracing hook behavior. - bench-compare.js: rework the SVG chart. Dynamic label column sized to the longest migration name (up to 420px), 80px reserved on the right for value labels so they never clip, inlined monospace font (SVG text doesn't reliably inherit CSS variables from the surrounding stylesheet), and `dominant-baseline="middle"` for proper vertical centering. Verified: full pipeline (setup:v4 → seed → snapshot → bench:run → bench:compare) works against postgres at multiplier=1. Ran a baseline vs cherry-picked PR #25988 comparison — captured all 7 v4→v5 migrations, produced both markdown and HTML reports with correct test-setup attribution and delta coloring. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(examples): run ANALYZE before db:check to get fresh row counts pg_stat_user_tables.n_live_tup and information_schema.tables.table_rows are approximate and can lag behind reality by minutes or hours depending on autovacuum / ANALYZE cadence. For a benchmark harness that publishes row-count numbers in its reports, stale counts are misleading. Trigger a refresh via ANALYZE (postgres) / ANALYZE TABLE per-table (mysql/mariadb) before each db:check invocation. Best-effort on the mysql/mariadb side — fall through to stale stats if ANALYZE fails rather than error the whole command. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(examples): add hc-m2m-source/target anti-pattern schemas First anti-pattern schema pair for migration benchmark stress-testing. A high-cardinality many-to-many relation that forces the v4→v5 discard-drafts migration's copyRelationTableRows code path to span multiple chunks (>1000 rows) — the same scenario PR #25988's caching fixes target. - src/api/hc-m2m-source: collection type with DP and a manyToMany relation to hc-m2m-target (owning side) - src/api/hc-m2m-target: collection type with DP and the inverse manyToMany back to source - setup-v4-project.js: include both in the v4 scaffold CONTENT_TYPES - seed-v4.js: seedHcM2m() method that creates sources + targets and fans out 10 targets-per-source via the M2M relation. BASE counts at m=1 are tiny (15 pub + 5 draft per side) but at m=100 produce ~2K sources × ~2K targets × 10 = 20K join rows, crossing the 1000-row chunk boundary multiple times Intentionally NOT a realistic content-type design — this is a stress-test fixture. See the description in schema.json. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(examples): render multiplier x db matrix in bench-compare Rework bench-compare to index results by (label, multiplier, dbEngine) triples pulled from each result JSON's own fields, rather than parsing labels out of filenames. Lets the same canonical baseline/candidate label span any number of (multiplier, db) combinations and produces: - A speedup matrix at the top: rows = multipliers, cols = databases, cells = "baseline -> candidate (delta%)". Missing cells render as "-" so partial data still produces a useful report. - A data-availability matrix listing what ran vs what's still missing. - Per-(db, multiplier) detail sections as collapsible details in HTML, all expanded in markdown. Also: - New flag syntax: --baseline <label> / --candidate <label>, with positional args kept for backward compat. - Legacy labels that embedded the multiplier (e.g. "baseline-m100") are normalized to their base form ("baseline"), letting older result files keep working. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(examples): force TCP for mysql/mariadb CLI in containers The mysql/mariadb CLI tools default to connecting via unix socket at /var/run/mysqld/mysqld.sock, which isn't populated in the official mysql:8 / mariadb:11 container images. Every invocation (check, snapshot, restore, wipe, readiness probe, version probe) needs an explicit -h 127.0.0.1 to force TCP via the container's loopback. Without this fix, bench:seed and bench:run error out with "Can't connect to local MySQL server through socket" on anything requiring the CLI inside the container (pg_stat-style row-count queries, snapshot restore, etc.). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * enhancement(examples): parallelize entity creation in seed-v4 Replace sequential for-loops of `await entityService.create(...)` with a `concurrentMap(count, concurrency, taskFn)` helper that runs N tasks in flight at once. At SEED_CONCURRENCY=5 (default), a seed that was strictly serial now fans out into 5 parallel creates. Concurrency chosen conservatively: Strapi v4's default knex pool is `{min: 2, max: 10}`, and entity-heavy creates (components + DZs + localizations) can use multiple connections per call. 5 keeps us well under the pool ceiling. Tune via `SEED_CONCURRENCY=<n>` env var if you've also raised the pool max. Applied to: seedBasic, seedBasicDp, updateComponentRelations, seedBasicDpI18n, seedRelation, seedRelationDp, seedRelationDpI18n, seedHcM2m (all entity-creation loops plus their follow-up self-reference update loops). Not yet done: incremental seeding (restore previous snapshot + seed delta) — a separate optimization tracked as a follow-up. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(examples): update complex README for new bench tooling + DBs README was documenting just the original 6-type, postgres+mysql workflow. Updated to cover everything this branch adds: - 8 content types (added hc-m2m-source/target anti-patterns) - 4 supported databases (added mariadb + sqlite) - Container runtime auto-detection (podman compose / podman-compose / docker compose / docker-compose) with STRAPI_BENCH_RUNTIME override - Benchmark harness workflow (bench:seed / bench:run / bench:compare / bench:suite) for reviewing migration-performance PRs - SEED_CONCURRENCY, STRAPI_BENCH_HOOK_OUTPUT, STRAPI_BENCH_HOOK_DEBUG, and the existing port-override env vars - MariaDB port default 3307 to avoid colliding with MySQL on 3306 Also collapsed the redundant per-DB command sections (postgres and mysql both had identical copy-pasted blocks) into a single 'yarn db:<op>:<db>' table since the commands are symmetric across all four dialects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(examples): align better-sqlite3 version with monorepo convention I picked `11.3.0` arbitrarily. Every other example and tests/app-template use `12.8.0`, and the root yarn.lock already resolves that version. Without alignment CI's `yarn install --immutable` fails with 'lockfile would have been modified', cascading every subsequent job (build, pretty, commitlint, aggregate_test_result) to red. Bumping to `12.8.0` to match, regenerating yarn.lock. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: throw instead of return to fail fast --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Ben Irvin <ben@innerdvations.com>
1084 lines
37 KiB
JavaScript
1084 lines
37 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
const strapi = require('@strapi/strapi')();
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const os = require('os');
|
||
|
||
// ============================================================================
|
||
// CONFIGURATION
|
||
// ============================================================================
|
||
|
||
const BASE_COUNTS = {
|
||
basic: 5,
|
||
basicDp: { published: 3, drafts: 2 },
|
||
basicDpI18n: { published: 3, drafts: 2 },
|
||
relation: 5,
|
||
relationDp: { published: 5, drafts: 3 },
|
||
relationDpI18n: { published: 5, drafts: 3 },
|
||
mediaFiles: 10,
|
||
// Anti-pattern: high-cardinality M2M. At m=100 this produces ~2000 sources
|
||
// × ~2000 targets, crossing the 1000-row chunk boundary in v4→v5 migrations
|
||
// so cross-chunk code paths actually get exercised. Keep targets per source
|
||
// modest (10) to avoid quadratic blow-up on disk.
|
||
hcM2mSource: { published: 15, drafts: 5 },
|
||
hcM2mTarget: { published: 15, drafts: 5 },
|
||
hcM2mTargetsPerSource: 10,
|
||
};
|
||
|
||
function parseCliArgs(argv) {
|
||
const opts = { multiplier: 1 };
|
||
|
||
for (let i = 0; i < argv.length; i += 1) {
|
||
const arg = argv[i];
|
||
if (arg === '--multiplier' && argv[i + 1] != null) {
|
||
opts.multiplier = Number(argv[i + 1]);
|
||
i += 1;
|
||
continue;
|
||
}
|
||
|
||
if (arg?.startsWith('--multiplier=')) {
|
||
opts.multiplier = Number(arg.split('=')[1]);
|
||
continue;
|
||
}
|
||
|
||
if (!Number.isNaN(Number(arg))) {
|
||
opts.multiplier = Number(arg);
|
||
}
|
||
}
|
||
|
||
const envMultiplier = process.env.SEED_MULTIPLIER;
|
||
if (!Number.isNaN(Number(envMultiplier))) {
|
||
opts.multiplier = Number(envMultiplier);
|
||
}
|
||
|
||
if (!Number.isFinite(opts.multiplier) || opts.multiplier <= 0) {
|
||
opts.multiplier = 1;
|
||
}
|
||
|
||
return opts;
|
||
}
|
||
|
||
function applyMultiplierToCounts(base, multiplier) {
|
||
const m = Number(multiplier) || 1;
|
||
return {
|
||
basic: base.basic * m,
|
||
basicDp: {
|
||
published: base.basicDp.published * m,
|
||
drafts: base.basicDp.drafts * m,
|
||
},
|
||
basicDpI18n: {
|
||
published: base.basicDpI18n.published * m,
|
||
drafts: base.basicDpI18n.drafts * m,
|
||
},
|
||
relation: base.relation * m,
|
||
relationDp: {
|
||
published: base.relationDp.published * m,
|
||
drafts: base.relationDp.drafts * m,
|
||
},
|
||
relationDpI18n: {
|
||
published: base.relationDpI18n.published * m,
|
||
drafts: base.relationDpI18n.drafts * m,
|
||
},
|
||
mediaFiles: base.mediaFiles * m,
|
||
hcM2mSource: {
|
||
published: base.hcM2mSource.published * m,
|
||
drafts: base.hcM2mSource.drafts * m,
|
||
},
|
||
hcM2mTarget: {
|
||
published: base.hcM2mTarget.published * m,
|
||
drafts: base.hcM2mTarget.drafts * m,
|
||
},
|
||
// Targets-per-source is intentionally NOT multiplied — it stays a constant
|
||
// fan-out so the total join-row count scales with the source count only.
|
||
hcM2mTargetsPerSource: base.hcM2mTargetsPerSource,
|
||
};
|
||
}
|
||
|
||
const { multiplier } = parseCliArgs(process.argv.slice(2));
|
||
|
||
const CONFIG = {
|
||
counts: applyMultiplierToCounts(BASE_COUNTS, multiplier),
|
||
locales: ['en', 'fr'],
|
||
};
|
||
|
||
// ============================================================================
|
||
// DATA GENERATORS
|
||
// ============================================================================
|
||
|
||
const random = {
|
||
string: (len = 8) =>
|
||
Math.random()
|
||
.toString(36)
|
||
.substring(2, len + 2),
|
||
number: (min = 0, max = 100) => Math.floor(Math.random() * (max - min + 1)) + min,
|
||
boolean: () => Math.random() > 0.5,
|
||
date: () => new Date(2020 + Math.random() * 5, random.number(0, 11), random.number(1, 28)),
|
||
pick: (arr) => arr[random.number(0, arr.length - 1)],
|
||
};
|
||
|
||
// ============================================================================
|
||
// CONCURRENCY HELPER
|
||
// ============================================================================
|
||
|
||
// Default concurrency for entity creation. Strapi v4's default knex pool is
|
||
// `{min: 2, max: 10}`, and relation-heavy entity creates (components + DZ +
|
||
// localizations) can use multiple connections per call. 5 keeps us well under
|
||
// the pool ceiling and still gives a meaningful speedup over strictly serial
|
||
// inserts. Tune up via SEED_CONCURRENCY=<n> env var if you've also raised the
|
||
// knex pool max in the v4 project's database config.
|
||
const SEED_CONCURRENCY = Number(process.env.SEED_CONCURRENCY) || 5;
|
||
|
||
/**
|
||
* Run `taskFn(i)` for i=0..count-1 with at most `concurrency` tasks in flight.
|
||
* Returns results in input order. Fails fast on the first task rejection.
|
||
*/
|
||
async function concurrentMap(count, concurrency, taskFn) {
|
||
const results = new Array(count);
|
||
let nextIndex = 0;
|
||
let firstError = null;
|
||
|
||
async function worker() {
|
||
while (firstError == null) {
|
||
const i = nextIndex;
|
||
if (i >= count) return;
|
||
nextIndex += 1;
|
||
try {
|
||
results[i] = await taskFn(i);
|
||
} catch (err) {
|
||
if (firstError == null) firstError = err;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
const workers = Array.from({ length: Math.min(concurrency, count) }, worker);
|
||
await Promise.all(workers);
|
||
if (firstError) throw firstError;
|
||
return results;
|
||
}
|
||
|
||
// ============================================================================
|
||
// FIELD FACTORIES
|
||
// ============================================================================
|
||
|
||
const fields = {
|
||
basic: () => ({
|
||
stringField: `String ${random.string()}`,
|
||
textField: `Text ${random.string(20)}`,
|
||
richText: `<p>Rich ${random.string(15)}</p>`,
|
||
integerField: random.number(1, 1000),
|
||
bigintegerField: random.number(1000000, 9999999),
|
||
decimalField: parseFloat((Math.random() * 100).toFixed(2)),
|
||
floatField: parseFloat((Math.random() * 100).toFixed(2)),
|
||
booleanField: random.boolean(),
|
||
dateField: random.date().toISOString().split('T')[0],
|
||
datetimeField: random.date().toISOString(),
|
||
timeField: `${String(random.number(0, 23)).padStart(2, '0')}:${String(random.number(0, 59)).padStart(2, '0')}:00`,
|
||
emailField: `test${random.string()}@example.com`,
|
||
passwordField: 'TestPassword123!',
|
||
jsonField: { key: random.string(), value: random.number() },
|
||
enumerationField: random.pick(['one', 'two', 'three']),
|
||
}),
|
||
};
|
||
|
||
// ============================================================================
|
||
// COMPONENT FACTORIES
|
||
// ============================================================================
|
||
|
||
const components = {
|
||
simpleInfo: () => ({
|
||
title: `Info ${random.string()}`,
|
||
description: `Description ${random.string(15)}`,
|
||
count: random.number(1, 100),
|
||
active: random.boolean(),
|
||
}),
|
||
|
||
imageBlock: () => ({
|
||
alt: `Image ${random.string()}`,
|
||
url: `https://example.com/images/${random.string()}.jpg`,
|
||
caption: `Caption ${random.string()}`,
|
||
width: random.number(100, 2000),
|
||
height: random.number(100, 2000),
|
||
}),
|
||
|
||
textBlock: (relations = {}) => ({
|
||
heading: `Heading ${random.string()}`,
|
||
body: `<p>Body ${random.string(20)}</p>`,
|
||
author: `Author ${random.string(4)}`,
|
||
publishedDate: random.date().toISOString().split('T')[0],
|
||
relatedBasic: relations.basicId || null,
|
||
relatedBasicDp: relations.basicDpId || null,
|
||
relatedRelationDp: relations.relationDpId || null,
|
||
}),
|
||
|
||
mediaBlock: () => ({
|
||
title: `Media ${random.string()}`,
|
||
mediaUrl: `https://example.com/media/${random.string()}.${random.pick(['jpg', 'mp4', 'mp3'])}`,
|
||
mediaType: random.pick(['image', 'video', 'audio']),
|
||
description: `Description ${random.string(15)}`,
|
||
}),
|
||
|
||
logo: (mediaId) => ({
|
||
name: `Logo ${random.string()}`,
|
||
logo: mediaId,
|
||
}),
|
||
|
||
header: (logoComponent) => ({
|
||
title: `Header ${random.string()}`,
|
||
headerlogo: logoComponent,
|
||
}),
|
||
|
||
reference: (articleId) => ({
|
||
label: `Ref ${random.string()}`,
|
||
article: articleId || null,
|
||
}),
|
||
|
||
referenceList: (references = []) => ({
|
||
title: `RefList ${random.string()}`,
|
||
references: Array.isArray(references) ? references : [references],
|
||
}),
|
||
|
||
// Dynamic zone wrappers
|
||
forDynamicZone: (component, type) => ({
|
||
__component: `shared.${type}`,
|
||
...component,
|
||
}),
|
||
};
|
||
|
||
// ============================================================================
|
||
// MEDIA FILE CREATION
|
||
// ============================================================================
|
||
|
||
const PNG_BUFFER = Buffer.from([
|
||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
|
||
0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
|
||
0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
|
||
0x42, 0x60, 0x82,
|
||
]);
|
||
|
||
async function createMediaFile(strapi, index) {
|
||
const name = `testimage${index}.png`;
|
||
const tempPath = path.join(os.tmpdir(), name);
|
||
|
||
try {
|
||
fs.writeFileSync(tempPath, PNG_BUFFER);
|
||
|
||
const file = {
|
||
filepath: tempPath,
|
||
path: tempPath,
|
||
originalFilename: name,
|
||
name: name,
|
||
size: PNG_BUFFER.length,
|
||
mimetype: 'image/png',
|
||
type: 'image/png',
|
||
};
|
||
|
||
const result = await strapi
|
||
.plugin('upload')
|
||
.service('upload')
|
||
.upload({
|
||
files: file,
|
||
data: {
|
||
fileInfo: {
|
||
alternativeText: `Test ${name}`,
|
||
caption: name,
|
||
name: name.replace('.png', ''),
|
||
},
|
||
},
|
||
});
|
||
|
||
fs.unlinkSync(tempPath);
|
||
return result[0];
|
||
} catch (error) {
|
||
console.error(`Failed to create media ${name}:`, error.message);
|
||
try {
|
||
fs.unlinkSync(tempPath);
|
||
} catch {}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function createMediaFiles(strapi, count) {
|
||
console.log(`Creating ${count} media files...`);
|
||
const files = [];
|
||
for (let i = 0; i < count; i++) {
|
||
const file = await createMediaFile(strapi, i + 1);
|
||
if (file) files.push(file);
|
||
}
|
||
console.log(`Created ${files.length} media files`);
|
||
return files;
|
||
}
|
||
|
||
// ============================================================================
|
||
// CONTENT TYPE SEEDERS
|
||
// ============================================================================
|
||
|
||
class ContentSeeder {
|
||
constructor(strapi, mediaFiles = []) {
|
||
this.strapi = strapi;
|
||
this.mediaFiles = mediaFiles;
|
||
this.results = {};
|
||
}
|
||
|
||
// Utility to pick items cyclically from arrays
|
||
pick(array, index, fallback = null) {
|
||
if (!array?.length) return fallback;
|
||
return array[index % array.length] || fallback;
|
||
}
|
||
|
||
// Generic error logger
|
||
logError(type, index, error) {
|
||
console.error(`Failed to create ${type} entry ${index}:`, error.message);
|
||
if (error.details?.errors) {
|
||
error.details.errors.forEach((err) => {
|
||
console.error(` - ${err.path || 'unknown'}: ${err.message}`);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Seed basic content type
|
||
async seedBasic() {
|
||
console.log('Seeding basic...');
|
||
const entries = await concurrentMap(CONFIG.counts.basic, SEED_CONCURRENCY, async (i) => {
|
||
try {
|
||
return await this.strapi.entityService.create('api::basic.basic', {
|
||
data: {
|
||
...fields.basic(),
|
||
textBlocks: [components.textBlock(), components.textBlock()],
|
||
mediaBlock: components.mediaBlock(),
|
||
sections: [
|
||
components.forDynamicZone(components.textBlock(), 'text-block'),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
],
|
||
},
|
||
});
|
||
} catch (error) {
|
||
this.logError('basic', i + 1, error);
|
||
throw error;
|
||
}
|
||
});
|
||
|
||
this.results.basic = entries;
|
||
return entries;
|
||
}
|
||
|
||
// Seed basic-dp content type
|
||
async seedBasicDp() {
|
||
console.log('Seeding basic-dp...');
|
||
|
||
const published = await concurrentMap(
|
||
CONFIG.counts.basicDp.published,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
const mediaFile = this.pick(this.mediaFiles, i);
|
||
const logo = mediaFile ? components.logo(mediaFile.id) : null;
|
||
const header = logo ? components.header(logo) : null;
|
||
|
||
return await this.strapi.entityService.create('api::basic-dp.basic-dp', {
|
||
data: {
|
||
...fields.basic(),
|
||
textBlocks: [components.textBlock(), components.textBlock()],
|
||
mediaBlock: components.mediaBlock(),
|
||
header: header,
|
||
sections: [
|
||
components.forDynamicZone(components.textBlock(), 'text-block'),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
...(header ? [components.forDynamicZone(components.header(logo), 'header')] : []),
|
||
],
|
||
publishedAt: new Date(),
|
||
},
|
||
});
|
||
} catch (error) {
|
||
this.logError('basic-dp published', i + 1, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
|
||
const drafts = await concurrentMap(
|
||
CONFIG.counts.basicDp.drafts,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
const mediaFile = this.pick(this.mediaFiles, i + 3);
|
||
const logo = mediaFile ? components.logo(mediaFile.id) : null;
|
||
const header = logo ? components.header(logo) : null;
|
||
|
||
return await this.strapi.entityService.create('api::basic-dp.basic-dp', {
|
||
data: {
|
||
...fields.basic(),
|
||
textBlocks: [components.textBlock(), components.textBlock()],
|
||
mediaBlock: components.mediaBlock(),
|
||
header: header,
|
||
sections: [
|
||
components.forDynamicZone(components.textBlock(), 'text-block'),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
...(header ? [components.forDynamicZone(components.header(logo), 'header')] : []),
|
||
],
|
||
},
|
||
});
|
||
} catch (error) {
|
||
this.logError('basic-dp draft', i + 1, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
|
||
this.results.basicDp = { published, drafts, all: [...published, ...drafts] };
|
||
return this.results.basicDp;
|
||
}
|
||
|
||
// Update component relations after initial creation
|
||
async updateComponentRelations() {
|
||
console.log('Updating component relations...');
|
||
const { basic, basicDp } = this.results;
|
||
|
||
if (!basic?.length || !basicDp?.published?.length) return;
|
||
|
||
await concurrentMap(basic.length, SEED_CONCURRENCY, async (i) => {
|
||
const publishedTarget = this.pick(basicDp.published, i);
|
||
const draftTarget = this.pick(basicDp.drafts, i, publishedTarget);
|
||
|
||
await this.strapi.entityService.update('api::basic.basic', basic[i].id, {
|
||
data: {
|
||
textBlocks: [
|
||
components.textBlock({ basicDpId: publishedTarget?.id }),
|
||
components.textBlock({ basicDpId: draftTarget?.id }),
|
||
],
|
||
sections: [
|
||
components.forDynamicZone(
|
||
components.textBlock({ basicDpId: draftTarget?.id }),
|
||
'text-block'
|
||
),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
],
|
||
},
|
||
});
|
||
});
|
||
|
||
await concurrentMap(basicDp.published.length, SEED_CONCURRENCY, async (i) => {
|
||
const target = this.pick(basicDp.published, i + 1);
|
||
await this.strapi.entityService.update('api::basic-dp.basic-dp', basicDp.published[i].id, {
|
||
data: {
|
||
textBlocks: [
|
||
components.textBlock({ basicId: this.pick(basic, i)?.id, basicDpId: target?.id }),
|
||
components.textBlock({ basicId: this.pick(basic, i + 1)?.id, basicDpId: target?.id }),
|
||
],
|
||
},
|
||
});
|
||
});
|
||
|
||
await concurrentMap(basicDp.drafts.length, SEED_CONCURRENCY, async (i) => {
|
||
const target = this.pick(basicDp.published, i);
|
||
await this.strapi.entityService.update('api::basic-dp.basic-dp', basicDp.drafts[i].id, {
|
||
data: {
|
||
textBlocks: [
|
||
components.textBlock({ basicId: this.pick(basic, i)?.id, basicDpId: target?.id }),
|
||
components.textBlock({ basicId: this.pick(basic, i + 1)?.id, basicDpId: target?.id }),
|
||
],
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
// Seed basic-dp-i18n content type
|
||
async seedBasicDpI18n() {
|
||
console.log('Seeding basic-dp-i18n...');
|
||
const published = [];
|
||
const drafts = [];
|
||
|
||
for (const locale of CONFIG.locales) {
|
||
const pub = await concurrentMap(
|
||
CONFIG.counts.basicDpI18n.published,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
return await this.strapi.entityService.create('api::basic-dp-i18n.basic-dp-i18n', {
|
||
data: {
|
||
...fields.basic(),
|
||
textBlocks: [components.textBlock(), components.textBlock()],
|
||
mediaBlock: components.mediaBlock(),
|
||
sections: [
|
||
components.forDynamicZone(components.textBlock(), 'text-block'),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
],
|
||
publishedAt: new Date(),
|
||
},
|
||
locale,
|
||
});
|
||
} catch (error) {
|
||
this.logError(`basic-dp-i18n published (${locale})`, i + 1, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
published.push(...pub);
|
||
|
||
const drf = await concurrentMap(
|
||
CONFIG.counts.basicDpI18n.drafts,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
return await this.strapi.entityService.create('api::basic-dp-i18n.basic-dp-i18n', {
|
||
data: {
|
||
...fields.basic(),
|
||
textBlocks: [components.textBlock(), components.textBlock()],
|
||
mediaBlock: components.mediaBlock(),
|
||
sections: [
|
||
components.forDynamicZone(components.textBlock(), 'text-block'),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
],
|
||
},
|
||
locale,
|
||
});
|
||
} catch (error) {
|
||
this.logError(`basic-dp-i18n draft (${locale})`, i + 1, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
drafts.push(...drf);
|
||
}
|
||
|
||
this.results.basicDpI18n = { published, drafts, all: [...published, ...drafts] };
|
||
return this.results.basicDpI18n;
|
||
}
|
||
|
||
// Seed relation content type
|
||
async seedRelation() {
|
||
console.log('Seeding relation...');
|
||
const { basic, basicDp } = this.results;
|
||
|
||
const entries = await concurrentMap(CONFIG.counts.relation, SEED_CONCURRENCY, async (i) => {
|
||
try {
|
||
const relatedBasics = [this.pick(basic, i), this.pick(basic, i + 1)].filter(Boolean);
|
||
const publishedDp = this.pick(basicDp?.published, i);
|
||
const draftDp = this.pick(basicDp?.drafts, i);
|
||
|
||
return await this.strapi.entityService.create('api::relation.relation', {
|
||
data: {
|
||
name: `Relation ${random.string()}`,
|
||
oneToOneBasic: relatedBasics[0]?.id || null,
|
||
oneToManyBasics: relatedBasics.map((b) => b.id),
|
||
manyToOneBasic: relatedBasics[0]?.id || null,
|
||
manyToManyBasics: relatedBasics.map((b) => b.id),
|
||
morph_to_one: relatedBasics[0]
|
||
? { __type: 'api::basic.basic', id: relatedBasics[0].id }
|
||
: null,
|
||
morph_to_many: relatedBasics.map((b) => ({ __type: 'api::basic.basic', id: b.id })),
|
||
simpleInfo: components.simpleInfo(),
|
||
content: [
|
||
components.forDynamicZone(components.simpleInfo(), 'simple-info'),
|
||
components.forDynamicZone(components.imageBlock(), 'image-block'),
|
||
],
|
||
textBlocks: [
|
||
components.textBlock({ basicId: relatedBasics[0]?.id, basicDpId: publishedDp?.id }),
|
||
components.textBlock({ basicId: relatedBasics[1]?.id, basicDpId: draftDp?.id }),
|
||
],
|
||
mediaBlock: components.mediaBlock(),
|
||
sections: [
|
||
components.forDynamicZone(
|
||
components.textBlock({ basicDpId: draftDp?.id }),
|
||
'text-block'
|
||
),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
],
|
||
},
|
||
});
|
||
} catch (error) {
|
||
this.logError('relation', i + 1, error);
|
||
throw error;
|
||
}
|
||
});
|
||
|
||
// Add self-references (parallelizable — each entry's update is independent).
|
||
await concurrentMap(entries.length, SEED_CONCURRENCY, async (i) => {
|
||
const entry = entries[i];
|
||
await this.strapi.entityService.update('api::relation.relation', entry.id, {
|
||
data: {
|
||
selfOne: entry.id,
|
||
selfMany: [entry.id],
|
||
},
|
||
});
|
||
});
|
||
|
||
this.results.relation = entries;
|
||
return entries;
|
||
}
|
||
|
||
// Seed relation-dp content type
|
||
async seedRelationDp() {
|
||
console.log('Seeding relation-dp...');
|
||
const { basic, basicDp, relation } = this.results;
|
||
const morphTargetsFor = (indices) =>
|
||
(relation || [])
|
||
.filter((_, j) => indices.includes(j))
|
||
.map((r) => ({ __type: 'api::relation.relation', id: r.id }));
|
||
|
||
const published = await concurrentMap(
|
||
CONFIG.counts.relationDp.published,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
const relatedDp = [
|
||
this.pick(basicDp?.published, i),
|
||
this.pick(basicDp?.drafts, i),
|
||
].filter(Boolean);
|
||
|
||
const relatedBasic = [this.pick(basic, i), this.pick(basic, i + 1)].filter(Boolean);
|
||
|
||
const mediaFile = this.pick(this.mediaFiles, i);
|
||
const logo = mediaFile ? components.logo(mediaFile.id) : null;
|
||
const header = logo ? components.header(logo) : null;
|
||
|
||
return await this.strapi.entityService.create('api::relation-dp.relation-dp', {
|
||
data: {
|
||
name: `Relation DP Published ${i + 1}`,
|
||
cover: mediaFile?.id ?? null,
|
||
morphTargets: morphTargetsFor([
|
||
i % (relation?.length || 1),
|
||
(i + 1) % (relation?.length || 1),
|
||
]),
|
||
oneToOneBasic: relatedDp[0]?.id || null,
|
||
oneToManyBasics: relatedDp.map((b) => b.id),
|
||
manyToOneBasic: relatedDp[0]?.id || null,
|
||
manyToManyBasics: relatedDp.map((b) => b.id),
|
||
manyToOneBasicNoDp: relatedBasic[0]?.id || null,
|
||
manyToManyBasicsNoDp: relatedBasic.map((b) => b.id),
|
||
simpleInfo: components.simpleInfo(),
|
||
content: [
|
||
components.forDynamicZone(components.simpleInfo(), 'simple-info'),
|
||
components.forDynamicZone(components.imageBlock(), 'image-block'),
|
||
],
|
||
textBlocks: [
|
||
components.textBlock({ basicDpId: relatedDp[0]?.id }),
|
||
components.textBlock({ basicDpId: relatedDp[1]?.id }),
|
||
],
|
||
mediaBlock: components.mediaBlock(),
|
||
header: header,
|
||
sections: [
|
||
components.forDynamicZone(
|
||
components.textBlock({ basicDpId: relatedDp[1]?.id }),
|
||
'text-block'
|
||
),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
...(header ? [components.forDynamicZone(components.header(logo), 'header')] : []),
|
||
],
|
||
publishedAt: new Date(),
|
||
},
|
||
});
|
||
} catch (error) {
|
||
this.logError('relation-dp published', i + 1, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
|
||
const drafts = await concurrentMap(
|
||
CONFIG.counts.relationDp.drafts,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
const relatedDp = [
|
||
this.pick(basicDp?.drafts, i),
|
||
this.pick(basicDp?.published, i),
|
||
].filter(Boolean);
|
||
|
||
const relatedBasic = [this.pick(basic, i), this.pick(basic, i + 1)].filter(Boolean);
|
||
|
||
const mediaFile = this.pick(this.mediaFiles, i + 5);
|
||
const logo = mediaFile ? components.logo(mediaFile.id) : null;
|
||
const header = logo ? components.header(logo) : null;
|
||
|
||
return await this.strapi.entityService.create('api::relation-dp.relation-dp', {
|
||
data: {
|
||
name: `Relation DP Draft ${i + 1}`,
|
||
cover: mediaFile?.id ?? null,
|
||
morphTargets: morphTargetsFor([
|
||
i % (relation?.length || 1),
|
||
(i + 2) % (relation?.length || 1),
|
||
]),
|
||
oneToOneBasic: relatedDp[0]?.id || null,
|
||
oneToManyBasics: relatedDp.map((b) => b.id),
|
||
manyToOneBasic: relatedDp[0]?.id || null,
|
||
manyToManyBasics: relatedDp.map((b) => b.id),
|
||
manyToOneBasicNoDp: relatedBasic[0]?.id || null,
|
||
manyToManyBasicsNoDp: relatedBasic.map((b) => b.id),
|
||
simpleInfo: components.simpleInfo(),
|
||
content: [
|
||
components.forDynamicZone(components.simpleInfo(), 'simple-info'),
|
||
components.forDynamicZone(components.imageBlock(), 'image-block'),
|
||
],
|
||
textBlocks: [
|
||
components.textBlock({ basicDpId: relatedDp[0]?.id }),
|
||
components.textBlock({ basicDpId: relatedDp[1]?.id }),
|
||
],
|
||
mediaBlock: components.mediaBlock(),
|
||
header: header,
|
||
sections: [
|
||
components.forDynamicZone(
|
||
components.textBlock({ basicDpId: relatedDp[0]?.id }),
|
||
'text-block'
|
||
),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
...(header ? [components.forDynamicZone(components.header(logo), 'header')] : []),
|
||
],
|
||
},
|
||
});
|
||
} catch (error) {
|
||
this.logError('relation-dp draft', i + 1, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
|
||
// Add self-references (each entry's update is independent, parallelizable).
|
||
const allEntries = [...published, ...drafts];
|
||
await concurrentMap(allEntries.length, SEED_CONCURRENCY, async (i) => {
|
||
const entry = allEntries[i];
|
||
await this.strapi.entityService.update('api::relation-dp.relation-dp', entry.id, {
|
||
data: {
|
||
selfOne: entry.id,
|
||
selfMany: [entry.id],
|
||
},
|
||
});
|
||
});
|
||
|
||
// Add nested component with relations (reference-list -> references -> article) to first published entry for migration test
|
||
if (published.length >= 2) {
|
||
const relatedDpForFirst = [
|
||
this.pick(basicDp?.published, 0),
|
||
this.pick(basicDp?.drafts, 0),
|
||
].filter(Boolean);
|
||
const mediaFileForFirst = this.pick(this.mediaFiles, 0);
|
||
const logoForFirst = mediaFileForFirst ? components.logo(mediaFileForFirst.id) : null;
|
||
const headerForFirst = logoForFirst ? components.header(logoForFirst) : null;
|
||
const refListSection = components.forDynamicZone(
|
||
components.referenceList([
|
||
components.reference(published[1]?.id),
|
||
components.reference(published[0]?.id),
|
||
]),
|
||
'reference-list'
|
||
);
|
||
await this.strapi.entityService.update('api::relation-dp.relation-dp', published[0].id, {
|
||
data: {
|
||
sections: [
|
||
components.forDynamicZone(
|
||
components.textBlock({ basicDpId: relatedDpForFirst[0]?.id }),
|
||
'text-block'
|
||
),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
...(headerForFirst
|
||
? [components.forDynamicZone(components.header(logoForFirst), 'header')]
|
||
: []),
|
||
refListSection,
|
||
],
|
||
},
|
||
});
|
||
}
|
||
|
||
this.results.relationDp = { published, drafts, all: [...published, ...drafts] };
|
||
return this.results.relationDp;
|
||
}
|
||
|
||
// Seed relation-dp-i18n content type
|
||
async seedRelationDpI18n() {
|
||
console.log('Seeding relation-dp-i18n...');
|
||
const published = [];
|
||
const drafts = [];
|
||
const { basicDpI18n } = this.results;
|
||
|
||
for (const locale of CONFIG.locales) {
|
||
const localeBasics = basicDpI18n?.all?.filter((b) => b.locale === locale) || [];
|
||
|
||
const pub = await concurrentMap(
|
||
CONFIG.counts.relationDpI18n.published,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
const related = [this.pick(localeBasics, i), this.pick(localeBasics, i + 1)].filter(
|
||
Boolean
|
||
);
|
||
return await this.strapi.entityService.create(
|
||
'api::relation-dp-i18n.relation-dp-i18n',
|
||
{
|
||
data: {
|
||
name: `Relation DP i18n Published ${i + 1}`,
|
||
oneToOneBasic: related[0]?.id || null,
|
||
oneToManyBasics: related.map((b) => b.id),
|
||
manyToOneBasic: related[0]?.id || null,
|
||
manyToManyBasics: related.map((b) => b.id),
|
||
simpleInfo: components.simpleInfo(),
|
||
content: [
|
||
components.forDynamicZone(components.simpleInfo(), 'simple-info'),
|
||
components.forDynamicZone(components.imageBlock(), 'image-block'),
|
||
],
|
||
textBlocks: [components.textBlock(), components.textBlock()],
|
||
mediaBlock: components.mediaBlock(),
|
||
sections: [
|
||
components.forDynamicZone(components.textBlock(), 'text-block'),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
],
|
||
publishedAt: new Date(),
|
||
},
|
||
locale,
|
||
}
|
||
);
|
||
} catch (error) {
|
||
this.logError(`relation-dp-i18n published (${locale})`, i + 1, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
published.push(...pub);
|
||
|
||
const drf = await concurrentMap(
|
||
CONFIG.counts.relationDpI18n.drafts,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
const related = [this.pick(localeBasics, i), this.pick(localeBasics, i + 1)].filter(
|
||
Boolean
|
||
);
|
||
return await this.strapi.entityService.create(
|
||
'api::relation-dp-i18n.relation-dp-i18n',
|
||
{
|
||
data: {
|
||
name: `Relation DP i18n Draft ${i + 1}`,
|
||
oneToOneBasic: related[0]?.id || null,
|
||
oneToManyBasics: related.map((b) => b.id),
|
||
manyToOneBasic: related[0]?.id || null,
|
||
manyToManyBasics: related.map((b) => b.id),
|
||
simpleInfo: components.simpleInfo(),
|
||
content: [
|
||
components.forDynamicZone(components.simpleInfo(), 'simple-info'),
|
||
components.forDynamicZone(components.imageBlock(), 'image-block'),
|
||
],
|
||
textBlocks: [components.textBlock(), components.textBlock()],
|
||
mediaBlock: components.mediaBlock(),
|
||
sections: [
|
||
components.forDynamicZone(components.textBlock(), 'text-block'),
|
||
components.forDynamicZone(components.mediaBlock(), 'media-block'),
|
||
],
|
||
},
|
||
locale,
|
||
}
|
||
);
|
||
} catch (error) {
|
||
this.logError(`relation-dp-i18n draft (${locale})`, i + 1, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
drafts.push(...drf);
|
||
}
|
||
|
||
const allEntries = [...published, ...drafts];
|
||
await concurrentMap(allEntries.length, SEED_CONCURRENCY, async (i) => {
|
||
const entry = allEntries[i];
|
||
await this.strapi.entityService.update('api::relation-dp-i18n.relation-dp-i18n', entry.id, {
|
||
data: {
|
||
selfOne: entry.id,
|
||
selfMany: [entry.id],
|
||
},
|
||
locale: entry.locale,
|
||
});
|
||
});
|
||
|
||
this.results.relationDpI18n = { published, drafts, all: [...published, ...drafts] };
|
||
return this.results.relationDpI18n;
|
||
}
|
||
|
||
// Run all seeders in sequence
|
||
// Seed the high-cardinality many-to-many pair. Anti-pattern by design:
|
||
// N sources × K targets-per-source join-table rows exercise the v4→v5
|
||
// `copyRelationTableRows` chunk-batched code path. With BASE=15/5 at m=100
|
||
// we get ~2000 sources × 2000 targets × 10 joins-per-source = 20K join
|
||
// rows, crossing the 1000-row chunk boundary multiple times.
|
||
async seedHcM2m() {
|
||
console.log('Seeding hc-m2m-target...');
|
||
const targetsPub = await concurrentMap(
|
||
CONFIG.counts.hcM2mTarget.published,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
return await this.strapi.entityService.create('api::hc-m2m-target.hc-m2m-target', {
|
||
data: {
|
||
label: `Target ${random.string(6)}`,
|
||
publishedAt: new Date(),
|
||
},
|
||
});
|
||
} catch (error) {
|
||
this.logError('hc-m2m-target published', i, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
const targetsDrf = await concurrentMap(
|
||
CONFIG.counts.hcM2mTarget.drafts,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
return await this.strapi.entityService.create('api::hc-m2m-target.hc-m2m-target', {
|
||
data: {
|
||
label: `Target draft ${random.string(6)}`,
|
||
publishedAt: null,
|
||
},
|
||
});
|
||
} catch (error) {
|
||
this.logError('hc-m2m-target draft', i, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
const targets = {
|
||
published: targetsPub.filter(Boolean),
|
||
drafts: targetsDrf.filter(Boolean),
|
||
};
|
||
|
||
const allTargets = [...targets.published, ...targets.drafts];
|
||
|
||
console.log('Seeding hc-m2m-source...');
|
||
const fanout = CONFIG.counts.hcM2mTargetsPerSource;
|
||
|
||
const pickTargetIds = (seedIdx) => {
|
||
if (!allTargets.length) return [];
|
||
const picks = [];
|
||
for (let j = 0; j < fanout; j += 1) {
|
||
picks.push(allTargets[(seedIdx + j) % allTargets.length].id);
|
||
}
|
||
// De-dup since modulo can repeat if fanout > allTargets.length.
|
||
return [...new Set(picks)];
|
||
};
|
||
|
||
const sourcesPub = await concurrentMap(
|
||
CONFIG.counts.hcM2mSource.published,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
return await this.strapi.entityService.create('api::hc-m2m-source.hc-m2m-source', {
|
||
data: {
|
||
label: `Source ${random.string(6)}`,
|
||
targets: pickTargetIds(i),
|
||
publishedAt: new Date(),
|
||
},
|
||
});
|
||
} catch (error) {
|
||
this.logError('hc-m2m-source published', i, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
const sourcesDrf = await concurrentMap(
|
||
CONFIG.counts.hcM2mSource.drafts,
|
||
SEED_CONCURRENCY,
|
||
async (i) => {
|
||
try {
|
||
return await this.strapi.entityService.create('api::hc-m2m-source.hc-m2m-source', {
|
||
data: {
|
||
label: `Source draft ${random.string(6)}`,
|
||
targets: pickTargetIds(i + CONFIG.counts.hcM2mSource.published),
|
||
publishedAt: null,
|
||
},
|
||
});
|
||
} catch (error) {
|
||
this.logError('hc-m2m-source draft', i, error);
|
||
throw error;
|
||
}
|
||
}
|
||
);
|
||
const sources = {
|
||
published: sourcesPub.filter(Boolean),
|
||
drafts: sourcesDrf.filter(Boolean),
|
||
};
|
||
|
||
this.results.hcM2mTarget = {
|
||
published: targets.published,
|
||
drafts: targets.drafts,
|
||
all: allTargets,
|
||
};
|
||
this.results.hcM2mSource = {
|
||
published: sources.published,
|
||
drafts: sources.drafts,
|
||
all: [...sources.published, ...sources.drafts],
|
||
};
|
||
return this.results.hcM2mSource;
|
||
}
|
||
|
||
async seedAll() {
|
||
await this.seedBasic();
|
||
await this.seedBasicDp();
|
||
await this.updateComponentRelations();
|
||
await this.seedBasicDpI18n();
|
||
await this.seedRelation();
|
||
await this.seedRelationDp();
|
||
await this.seedRelationDpI18n();
|
||
await this.seedHcM2m();
|
||
return this.results;
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// MAIN EXECUTION
|
||
// ============================================================================
|
||
|
||
async function seed() {
|
||
console.log('🌱 Starting simplified seed...\n');
|
||
|
||
try {
|
||
await strapi.load();
|
||
|
||
// Create media files
|
||
const mediaFiles = await createMediaFiles(strapi, CONFIG.counts.mediaFiles);
|
||
|
||
// Run seeder
|
||
const seeder = new ContentSeeder(strapi, mediaFiles);
|
||
const results = await seeder.seedAll();
|
||
|
||
// Display summary
|
||
console.log('\n✅ Seed completed successfully!');
|
||
console.log('\nCreated:');
|
||
console.log(` - ${results.basic?.length || 0} basic entries`);
|
||
console.log(
|
||
` - ${results.basicDp?.all?.length || 0} basic-dp entries (${results.basicDp?.published?.length || 0} published, ${results.basicDp?.drafts?.length || 0} drafts)`
|
||
);
|
||
console.log(` - ${results.basicDpI18n?.all?.length || 0} basic-dp-i18n entries`);
|
||
console.log(` - ${results.relation?.length || 0} relation entries`);
|
||
console.log(` - ${results.relationDp?.all?.length || 0} relation-dp entries`);
|
||
console.log(` - ${results.relationDpI18n?.all?.length || 0} relation-dp-i18n entries`);
|
||
console.log(
|
||
` - ${results.hcM2mSource?.all?.length || 0} hc-m2m-source entries (${results.hcM2mSource?.published?.length || 0} published, ${results.hcM2mSource?.drafts?.length || 0} drafts)`
|
||
);
|
||
console.log(
|
||
` - ${results.hcM2mTarget?.all?.length || 0} hc-m2m-target entries (${results.hcM2mTarget?.published?.length || 0} published, ${results.hcM2mTarget?.drafts?.length || 0} drafts)`
|
||
);
|
||
console.log(` - ${mediaFiles.length} media files`);
|
||
} catch (error) {
|
||
console.error('\n❌ Error seeding data:', error.message);
|
||
if (error.details?.errors) {
|
||
console.error('\nValidation errors:');
|
||
error.details.errors.forEach((err, i) => {
|
||
console.error(` ${i + 1}. ${err.path || 'unknown'}: ${err.message}`);
|
||
});
|
||
}
|
||
throw error;
|
||
} finally {
|
||
await strapi.destroy();
|
||
}
|
||
}
|
||
|
||
// CLI execution
|
||
if (require.main === module) {
|
||
seed()
|
||
.then(() => process.exit(0))
|
||
.catch((error) => {
|
||
console.error(error);
|
||
process.exit(1);
|
||
});
|
||
}
|
||
|
||
module.exports = seed;
|