/
githubmirror
/
strapi
Обзор
Документация
Войти
/
githubmirror
/
strapi
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
examples/complex/scripts/seed-v4.ts
999 строк
35 KB
Ben Irvin
test(migration): version migration testing framework (#26060)
06 авг 2026, 15:39
Не верифицирован
06 авг 2026, 15:39
ac0c928
Код
Авторство
О чём код?
#!/usr/bin/env node const strapi = require('@strapi/strapi')(); const fs = require('fs'); const path = require('path'); const os = require('os'); // ============================================================================ // CONFIGURATION // ============================================================================ const { requireFixture } = require('./require-fixture'); const spec = requireFixture('spec'); const { getSeedCountsForProfile } = requireFixture('derive-expectations'); const { parseMultiplier } = requireFixture('resolve-context'); const multiplier = parseMultiplier(process.argv.slice(2)); const CONFIG = { counts: getSeedCountsForProfile(spec, { profile: 'v4', multiplier }), locales: spec.locales, }; // ============================================================================ // 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; } }); // Self-relations touch the same join row from both sides; keep serial to avoid MySQL deadlocks. for (const entry of entries) { 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; } } ); // Self-relations touch the same join row from both sides; keep serial to avoid MySQL deadlocks. const allEntries = [...published, ...drafts]; for (const entry of allEntries) { 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]; for (const entry of allEntries) { 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;