@skmdev/prisma-fixtures 0.1.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.
@@ -0,0 +1,349 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DANGEROUS_KEYS = exports.MAX_FIXTURE_DEFINITIONS = exports.FixtureDocumentError = void 0;
7
+ exports.fixtureErrorContext = fixtureErrorContext;
8
+ exports.inheritFixtureSource = inheritFixtureSource;
9
+ exports.readFixturePaths = readFixturePaths;
10
+ exports.readFixtureDocuments = readFixtureDocuments;
11
+ exports.assertFixtureDefinition = assertFixtureDefinition;
12
+ exports.isFixtureRecord = isFixtureRecord;
13
+ exports.assertSafeFixtureValue = assertSafeFixtureValue;
14
+ const node_fs_1 = __importDefault(require("node:fs"));
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ const yaml_1 = require("yaml");
17
+ const fixture_error_1 = require("./fixture-error");
18
+ // Only messages constructed here are safe to display as lint diagnostics.
19
+ class FixtureDocumentError extends fixture_error_1.FixtureError {
20
+ constructor(message, context = {}, cause) {
21
+ super('FIXTURE_DOCUMENT_INVALID', message, { stage: 'reading fixtures', ...context }, cause);
22
+ this.name = 'FixtureDocumentError';
23
+ (0, fixture_error_1.markFixtureErrorTrusted)(this);
24
+ }
25
+ }
26
+ exports.FixtureDocumentError = FixtureDocumentError;
27
+ const fixtureSources = new WeakMap();
28
+ function fixtureErrorContext(fixture, stage, path) {
29
+ if (!fixture)
30
+ return { stage, path };
31
+ return {
32
+ stage,
33
+ file: fixtureSources.get(fixture)?.file,
34
+ fixtureName: fixture.name,
35
+ entity: fixture.entity,
36
+ ...(path === undefined ? {} : { path }),
37
+ };
38
+ }
39
+ function inheritFixtureSource(source, target) {
40
+ const provenance = fixtureSources.get(source);
41
+ if (provenance)
42
+ fixtureSources.set(target, provenance);
43
+ }
44
+ const MAX_FILE_BYTES = 1024 * 1024;
45
+ const MAX_FILES = 100;
46
+ exports.MAX_FIXTURE_DEFINITIONS = 2000;
47
+ const MAX_DEPTH = 32;
48
+ const MAX_NODES = 50_000;
49
+ exports.DANGEROUS_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
50
+ const NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,127}$/;
51
+ const FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
52
+ const RANGE_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*)\{(\d+)\.\.(\d+)\}$/;
53
+ const CURRENT_PATTERN = /\(\$current(?:([+\-*/])(\d+))?\)/g;
54
+ function readFixturePaths(targets, validateDocument) {
55
+ const definitions = targets.flatMap((target) => readFixtureDocuments(node_path_1.default.resolve(target), validateDocument));
56
+ if (new Set(definitions.map(({ name }) => name)).size !== definitions.length) {
57
+ throw new FixtureDocumentError('Duplicate fixture name across paths');
58
+ }
59
+ return definitions;
60
+ }
61
+ function readFixtureDocuments(targetPath, validateDocument) {
62
+ const definitions = [];
63
+ for (const file of collectFiles(targetPath)) {
64
+ const document = readDocument(file);
65
+ const normalized = normalizeDocument(document, file, exports.MAX_FIXTURE_DEFINITIONS - definitions.length);
66
+ validateDocument?.(document, file);
67
+ definitions.push(...normalized);
68
+ }
69
+ const names = new Set();
70
+ for (const { name } of definitions) {
71
+ if (names.has(name))
72
+ throw new FixtureDocumentError('Duplicate fixture name', {
73
+ fixtureName: name,
74
+ });
75
+ names.add(name);
76
+ }
77
+ return definitions;
78
+ }
79
+ function collectFiles(targetPath) {
80
+ let stat;
81
+ try {
82
+ stat = node_fs_1.default.statSync(targetPath);
83
+ }
84
+ catch {
85
+ throw new FixtureDocumentError(`Fixture path not found: ${JSON.stringify(targetPath)}`);
86
+ }
87
+ if (stat.isFile()) {
88
+ if (!/\.(json|ya?ml)$/i.test(targetPath)) {
89
+ throw new FixtureDocumentError('Fixture file must use .json, .yml or .yaml');
90
+ }
91
+ return [targetPath];
92
+ }
93
+ if (!stat.isDirectory())
94
+ throw new FixtureDocumentError('Fixture path is not a file or directory');
95
+ const files = node_fs_1.default
96
+ .readdirSync(targetPath, { withFileTypes: true })
97
+ .filter((entry) => entry.isFile() && /\.(json|ya?ml)$/i.test(entry.name))
98
+ .map((entry) => node_path_1.default.join(targetPath, entry.name))
99
+ .sort();
100
+ if (files.length > MAX_FILES)
101
+ throw new FixtureDocumentError('Too many fixture files');
102
+ return files;
103
+ }
104
+ function readDocument(file) {
105
+ if (node_fs_1.default.statSync(file).size > MAX_FILE_BYTES) {
106
+ throw new FixtureDocumentError(`Fixture file is too large: ${JSON.stringify(node_path_1.default.basename(file))}`);
107
+ }
108
+ const raw = node_fs_1.default.readFileSync(file, 'utf8');
109
+ try {
110
+ const value = /\.json$/i.test(file)
111
+ ? parseJson(raw)
112
+ : parseYaml(raw);
113
+ assertSafeFixtureValue(value);
114
+ return value;
115
+ }
116
+ catch (error) {
117
+ const detail = error instanceof FixtureDocumentError ? ` (${error.message})` : '';
118
+ throw new FixtureDocumentError(`Invalid fixture document: ${JSON.stringify(node_path_1.default.basename(file))}${detail}`, { file: node_path_1.default.basename(file) }, error);
119
+ }
120
+ }
121
+ function parseJson(raw) {
122
+ const value = JSON.parse(raw);
123
+ // JSON.parse keeps JSON scalar semantics but discards duplicate keys; YAML checks
124
+ // the original key structure without replacing the native JSON result.
125
+ parseYaml(raw);
126
+ return value;
127
+ }
128
+ function parseYaml(raw) {
129
+ const lineCounter = new yaml_1.LineCounter();
130
+ const document = (0, yaml_1.parseDocument)(raw, {
131
+ lineCounter,
132
+ prettyErrors: false,
133
+ strict: true,
134
+ stringKeys: true,
135
+ uniqueKeys: true,
136
+ });
137
+ const issue = document.errors[0] ?? document.warnings[0];
138
+ if (issue) {
139
+ const { line, col } = lineCounter.linePos(issue.pos[0]);
140
+ throw new FixtureDocumentError(`${issue.code} at ${line}:${col}`);
141
+ }
142
+ return document.toJS({ maxAliasCount: 0 });
143
+ }
144
+ function normalizeDocument(value, file, remaining) {
145
+ if (!isFixtureRecord(value))
146
+ throw invalidDocument(file);
147
+ assertKeys(value, [
148
+ 'entity',
149
+ 'locale',
150
+ 'parameters',
151
+ 'processor',
152
+ 'connectedFields',
153
+ 'deferredFields',
154
+ 'items',
155
+ ], file);
156
+ const { entity, locale, parameters = {}, processor, connectedFields, deferredFields, items, } = value;
157
+ if (!isFixtureRecord(items))
158
+ throw invalidDocument(file);
159
+ assertFixtureMetadata({ ...value, parameters }, file);
160
+ const definitions = [];
161
+ const fixtureProcessor = typeof processor === 'string'
162
+ ? node_path_1.default.resolve(node_path_1.default.dirname(file), processor)
163
+ : undefined;
164
+ const fixtureConnectedFields = connectedFields;
165
+ const fixtureDeferredFields = deferredFields;
166
+ for (const [rawName, rawData] of Object.entries(items)) {
167
+ if (!isFixtureRecord(rawData))
168
+ throw invalidDocument(file);
169
+ const expandedNames = expandName(rawName, file);
170
+ if (expandedNames.length > remaining - definitions.length) {
171
+ throw new FixtureDocumentError('Too many fixture definitions');
172
+ }
173
+ for (const { name, current } of expandedNames) {
174
+ const definition = {
175
+ name,
176
+ entity,
177
+ data: replaceCurrent(rawData, current, file),
178
+ parameters: structuredClone(parameters),
179
+ ...(fixtureProcessor === undefined
180
+ ? {}
181
+ : { processor: fixtureProcessor }),
182
+ ...(locale === undefined ? {} : { locale }),
183
+ ...(fixtureConnectedFields === undefined
184
+ ? {}
185
+ : { connectedFields: [...fixtureConnectedFields] }),
186
+ ...(fixtureDeferredFields === undefined
187
+ ? {}
188
+ : { deferredFields: [...fixtureDeferredFields] }),
189
+ };
190
+ try {
191
+ assertFixtureDefinition(definition);
192
+ }
193
+ catch {
194
+ throw invalidDocument(file);
195
+ }
196
+ fixtureSources.set(definition, { file: node_path_1.default.basename(file) });
197
+ definitions.push(definition);
198
+ }
199
+ }
200
+ return definitions;
201
+ }
202
+ function expandName(rawName, file) {
203
+ const match = rawName.match(RANGE_PATTERN);
204
+ if (!match) {
205
+ if (!NAME_PATTERN.test(rawName) || exports.DANGEROUS_KEYS.has(rawName)) {
206
+ throw invalidDocument(file);
207
+ }
208
+ const suffix = rawName.match(/(\d+)$/)?.[1];
209
+ return [
210
+ {
211
+ name: rawName,
212
+ current: suffix === undefined ? undefined : Number(suffix),
213
+ },
214
+ ];
215
+ }
216
+ const start = Number(match[2]);
217
+ const end = Number(match[3]);
218
+ if (!Number.isSafeInteger(start) ||
219
+ !Number.isSafeInteger(end) ||
220
+ start > end ||
221
+ end - start + 1 > exports.MAX_FIXTURE_DEFINITIONS) {
222
+ throw invalidDocument(file);
223
+ }
224
+ return Array.from({ length: end - start + 1 }, (_, offset) => ({
225
+ name: `${match[1]}${start + offset}`,
226
+ current: start + offset,
227
+ }));
228
+ }
229
+ function replaceCurrent(value, current, file) {
230
+ if (typeof value === 'string') {
231
+ const replaced = value.replace(CURRENT_PATTERN, (_token, operator, rawOperand) => {
232
+ if (current === undefined)
233
+ throw invalidDocument(file);
234
+ const operand = rawOperand === undefined ? undefined : Number(rawOperand);
235
+ const result = calculateCurrent(current, operator, operand);
236
+ if (!Number.isFinite(result))
237
+ throw invalidDocument(file);
238
+ return String(result);
239
+ });
240
+ if (replaced.includes('($current'))
241
+ throw invalidDocument(file);
242
+ return replaced;
243
+ }
244
+ if (Array.isArray(value)) {
245
+ return value.map((item) => replaceCurrent(item, current, file));
246
+ }
247
+ if (isFixtureRecord(value)) {
248
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
249
+ key,
250
+ replaceCurrent(item, current, file),
251
+ ]));
252
+ }
253
+ return value;
254
+ }
255
+ function calculateCurrent(current, operator, operand) {
256
+ if (!operator || operand === undefined)
257
+ return current;
258
+ if (operator === '+')
259
+ return current + operand;
260
+ if (operator === '-')
261
+ return current - operand;
262
+ if (operator === '*')
263
+ return current * operand;
264
+ if (operator === '/')
265
+ return current / operand;
266
+ throw new Error('Invalid current operation');
267
+ }
268
+ function assertFixtureDefinition(value) {
269
+ if (!isFixtureRecord(value))
270
+ throw new Error('Invalid fixture definition');
271
+ assertKeys(value, [
272
+ 'name',
273
+ 'entity',
274
+ 'data',
275
+ 'parameters',
276
+ 'processor',
277
+ 'locale',
278
+ 'connectedFields',
279
+ 'deferredFields',
280
+ ]);
281
+ const { name, data, deferredFields } = value;
282
+ assertFixtureMetadata(value);
283
+ if (typeof name !== 'string' ||
284
+ !NAME_PATTERN.test(name) ||
285
+ exports.DANGEROUS_KEYS.has(name) ||
286
+ !isFixtureRecord(data)) {
287
+ throw new Error('Invalid fixture definition');
288
+ }
289
+ if (Array.isArray(deferredFields) &&
290
+ deferredFields.some((field) => field === 'id' || !Object.hasOwn(data, field))) {
291
+ throw new Error('Invalid deferred fixture field');
292
+ }
293
+ assertSafeFixtureValue(value);
294
+ }
295
+ function assertFixtureMetadata(metadata, file) {
296
+ const { entity, parameters, processor, locale, connectedFields, deferredFields, } = metadata;
297
+ if (typeof entity !== 'string' ||
298
+ !NAME_PATTERN.test(entity) ||
299
+ exports.DANGEROUS_KEYS.has(entity) ||
300
+ !isFixtureRecord(parameters) ||
301
+ (processor !== undefined &&
302
+ (typeof processor !== 'string' || !processor)) ||
303
+ (locale !== undefined && (typeof locale !== 'string' || !locale)) ||
304
+ (connectedFields !== undefined && !isFieldList(connectedFields)) ||
305
+ (deferredFields !== undefined &&
306
+ (!isFieldList(deferredFields) ||
307
+ deferredFields.length === 0 ||
308
+ deferredFields.some((field) => Array.isArray(connectedFields) && connectedFields.includes(field))))) {
309
+ throw file ? invalidDocument(file) : new Error('Invalid fixture definition');
310
+ }
311
+ }
312
+ function isFieldList(value) {
313
+ return (Array.isArray(value) &&
314
+ value.every((field) => typeof field === 'string' &&
315
+ FIELD_PATTERN.test(field) &&
316
+ !exports.DANGEROUS_KEYS.has(field)) &&
317
+ new Set(value).size === value.length);
318
+ }
319
+ function isFixtureRecord(value) {
320
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
321
+ return false;
322
+ const prototype = Object.getPrototypeOf(value);
323
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
324
+ }
325
+ function assertSafeFixtureValue(value) {
326
+ let nodes = 0;
327
+ const visit = (current, depth) => {
328
+ if (++nodes > MAX_NODES || depth > MAX_DEPTH)
329
+ throw new Error('Fixture data is too deep');
330
+ if (Array.isArray(current))
331
+ return current.forEach((item) => visit(item, depth + 1));
332
+ if (!isFixtureRecord(current))
333
+ return;
334
+ for (const [key, item] of Object.entries(current)) {
335
+ if (exports.DANGEROUS_KEYS.has(key))
336
+ throw new Error('Unsafe fixture key');
337
+ visit(item, depth + 1);
338
+ }
339
+ };
340
+ visit(value, 0);
341
+ }
342
+ function assertKeys(value, allowed, file) {
343
+ if (Object.keys(value).some((key) => !allowed.includes(key))) {
344
+ throw file ? invalidDocument(file) : new Error('Invalid fixture definition');
345
+ }
346
+ }
347
+ function invalidDocument(file) {
348
+ return new FixtureDocumentError(`Invalid fixture document: ${JSON.stringify(node_path_1.default.basename(file))}`, { file: node_path_1.default.basename(file) });
349
+ }
@@ -0,0 +1,22 @@
1
+ export type FixtureErrorCode = 'FIXTURE_OPTIONS_INVALID' | 'FIXTURE_DOCUMENT_INVALID' | 'FIXTURE_REFERENCE_INVALID' | 'FIXTURE_REFERENCE_MISSING' | 'FIXTURE_REFERENCE_FIELD_MISSING' | 'FIXTURE_DEPENDENCY_CYCLE' | 'FIXTURE_TEMPLATE_FAILED' | 'FIXTURE_PROCESSOR_FAILED' | 'FIXTURE_CONNECTION_FAILED' | 'FIXTURE_WRITE_FAILED';
2
+ export type FixtureErrorContext = {
3
+ stage: string;
4
+ file?: string;
5
+ fixtureName?: string;
6
+ entity?: string;
7
+ path?: string;
8
+ };
9
+ export declare class FixtureError extends Error {
10
+ readonly code: FixtureErrorCode;
11
+ readonly stage: string;
12
+ readonly file?: string;
13
+ readonly fixtureName?: string;
14
+ readonly entity?: string;
15
+ readonly path?: string;
16
+ constructor(code: FixtureErrorCode, message: string, context: FixtureErrorContext, cause?: unknown);
17
+ }
18
+ export declare function createFixtureError(code: FixtureErrorCode, message: string, context: FixtureErrorContext, cause?: unknown): FixtureError;
19
+ export declare function markFixtureErrorTrusted<T extends FixtureError>(error: T): T;
20
+ export declare function isTrustedFixtureError(error: unknown): error is FixtureError;
21
+ export declare function wrapFixtureError(error: unknown, code: FixtureErrorCode, message: string, context: FixtureErrorContext): FixtureError;
22
+ export declare function appendFixturePath(path: string, part: string | number): string;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FixtureError = void 0;
4
+ exports.createFixtureError = createFixtureError;
5
+ exports.markFixtureErrorTrusted = markFixtureErrorTrusted;
6
+ exports.isTrustedFixtureError = isTrustedFixtureError;
7
+ exports.wrapFixtureError = wrapFixtureError;
8
+ exports.appendFixturePath = appendFixturePath;
9
+ const trustedFixtureErrors = new WeakSet();
10
+ class FixtureError extends Error {
11
+ code;
12
+ stage;
13
+ file;
14
+ fixtureName;
15
+ entity;
16
+ path;
17
+ constructor(code, message, context, cause) {
18
+ super(message, cause === undefined ? undefined : { cause });
19
+ this.name = 'FixtureError';
20
+ this.code = code;
21
+ this.stage = context.stage;
22
+ this.file = context.file;
23
+ this.fixtureName = context.fixtureName;
24
+ this.entity = context.entity;
25
+ this.path = context.path;
26
+ }
27
+ }
28
+ exports.FixtureError = FixtureError;
29
+ function createFixtureError(code, message, context, cause) {
30
+ return markFixtureErrorTrusted(new FixtureError(code, message, context, cause));
31
+ }
32
+ function markFixtureErrorTrusted(error) {
33
+ trustedFixtureErrors.add(error);
34
+ return Object.freeze(error);
35
+ }
36
+ function isTrustedFixtureError(error) {
37
+ return error instanceof FixtureError && trustedFixtureErrors.has(error);
38
+ }
39
+ function wrapFixtureError(error, code, message, context) {
40
+ if (isTrustedFixtureError(error)) {
41
+ return createFixtureError(error.code, error.message, {
42
+ stage: error.stage || context.stage,
43
+ file: error.file ?? context.file,
44
+ fixtureName: error.fixtureName ?? context.fixtureName,
45
+ entity: error.entity ?? context.entity,
46
+ path: error.path ?? context.path,
47
+ }, error.cause);
48
+ }
49
+ return createFixtureError(code, message, context, error);
50
+ }
51
+ function appendFixturePath(path, part) {
52
+ const token = String(part).replaceAll('~', '~0').replaceAll('/', '~1');
53
+ return `${path}/${token}`;
54
+ }
@@ -0,0 +1,17 @@
1
+ import { type FixtureDefinition } from './fixture-document';
2
+ type Dependency = {
3
+ name: string;
4
+ path: string;
5
+ };
6
+ export type PreparedFixture = FixtureDefinition & {
7
+ dependencies: Dependency[];
8
+ };
9
+ export declare function lintFixtureReferences(definitions: FixtureDefinition[]): {
10
+ unresolved: number;
11
+ };
12
+ export declare function prepareFixtureReferences(definitions: FixtureDefinition[], random?: {
13
+ next: () => number;
14
+ }): PreparedFixture[];
15
+ export declare function resolveFixtureReferences(value: unknown, records: Record<string, Record<string, unknown>>, fixture?: FixtureDefinition, path?: string): unknown;
16
+ export declare function applyFixtureConnections(data: Record<string, unknown>, connectedFields: string[] | undefined, fixture?: FixtureDefinition): Record<string, unknown>;
17
+ export {};
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.lintFixtureReferences = lintFixtureReferences;
4
+ exports.prepareFixtureReferences = prepareFixtureReferences;
5
+ exports.resolveFixtureReferences = resolveFixtureReferences;
6
+ exports.applyFixtureConnections = applyFixtureConnections;
7
+ const fixture_document_1 = require("./fixture-document");
8
+ const fixture_error_1 = require("./fixture-error");
9
+ const FIXED_REFERENCE = /^@([A-Za-z][A-Za-z0-9_-]{0,127})(?:\.([A-Za-z][A-Za-z0-9_]*))?$/;
10
+ const WILDCARD_REFERENCE = /^@([A-Za-z][A-Za-z0-9_-]{0,126})\*$/;
11
+ const RANGE_REFERENCE = /^@([A-Za-z][A-Za-z0-9_-]*)\{(\d+)\.\.(\d+)\}$/;
12
+ const DYNAMIC_VALUE = /<%|{{|<{/;
13
+ function lintFixtureReferences(definitions) {
14
+ const byName = new Map(definitions.map((fixture) => [fixture.name, fixture]));
15
+ const dependencies = new Map();
16
+ let unresolved = 0;
17
+ for (const fixture of definitions) {
18
+ const found = [];
19
+ unresolved += lintValue(fixture.data, byName, found, fixture, '');
20
+ dependencies.set(fixture.name, found);
21
+ }
22
+ orderFixtures(definitions, dependencies, 'linting references');
23
+ return { unresolved };
24
+ }
25
+ function lintValue(value, definitions, dependencies, fixture, path) {
26
+ if (typeof value === 'string') {
27
+ if (DYNAMIC_VALUE.test(value))
28
+ return 1;
29
+ if (!value.startsWith('@') || value.startsWith('@@'))
30
+ return 0;
31
+ const fixed = value.match(FIXED_REFERENCE);
32
+ if (fixed) {
33
+ const [, name, field] = fixed;
34
+ if (!definitions.has(name)) {
35
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_MISSING', 'Fixture reference target not found', (0, fixture_document_1.fixtureErrorContext)(fixture, 'linting references', path));
36
+ }
37
+ if (field && fixture_document_1.DANGEROUS_KEYS.has(field)) {
38
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_INVALID', 'Unsafe fixture reference field', (0, fixture_document_1.fixtureErrorContext)(fixture, 'linting references', path));
39
+ }
40
+ dependencies.push({ name: name, path });
41
+ return 0;
42
+ }
43
+ const candidates = referenceCandidates(value, definitions, fixture, path, 'linting references');
44
+ if (!candidates) {
45
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_INVALID', 'Invalid fixture reference', (0, fixture_document_1.fixtureErrorContext)(fixture, 'linting references', path));
46
+ }
47
+ if (candidates.length === 1) {
48
+ dependencies.push({ name: candidates[0], path });
49
+ return 0;
50
+ }
51
+ return 1;
52
+ }
53
+ if (Array.isArray(value)) {
54
+ return value.reduce((count, item, index) => count +
55
+ lintValue(item, definitions, dependencies, fixture, (0, fixture_error_1.appendFixturePath)(path, index)), 0);
56
+ }
57
+ if ((0, fixture_document_1.isFixtureRecord)(value)) {
58
+ return Object.entries(value).reduce((count, [key, item]) => count +
59
+ lintValue(item, definitions, dependencies, fixture, (0, fixture_error_1.appendFixturePath)(path, key)), 0);
60
+ }
61
+ return 0;
62
+ }
63
+ function prepareFixtureReferences(definitions, random = { next: Math.random }) {
64
+ const byName = new Map(definitions.map((fixture) => [fixture.name, fixture]));
65
+ const prepared = definitions.map((fixture) => {
66
+ const dependencies = [];
67
+ const data = prepareValue(fixture.data, byName, dependencies, random, fixture, '');
68
+ const result = {
69
+ ...fixture,
70
+ data: data,
71
+ dependencies,
72
+ };
73
+ (0, fixture_document_1.inheritFixtureSource)(fixture, result);
74
+ return result;
75
+ });
76
+ return orderFixtures(prepared, new Map(prepared.map(({ name, dependencies }) => [name, dependencies])), 'ordering fixtures');
77
+ }
78
+ function prepareValue(value, definitions, dependencies, random, fixture, path) {
79
+ if (typeof value === 'string') {
80
+ if (!value.startsWith('@') || value.startsWith('@@'))
81
+ return value;
82
+ const selected = selectReference(value, definitions, random, fixture, path);
83
+ const match = selected.match(FIXED_REFERENCE);
84
+ if (!match) {
85
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_INVALID', 'Invalid fixture reference', (0, fixture_document_1.fixtureErrorContext)(fixture, 'preparing references', path));
86
+ }
87
+ const [, name, field] = match;
88
+ const target = definitions.get(name);
89
+ if (!target) {
90
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_MISSING', 'Fixture reference target not found', (0, fixture_document_1.fixtureErrorContext)(fixture, 'preparing references', path));
91
+ }
92
+ if (field && fixture_document_1.DANGEROUS_KEYS.has(field)) {
93
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_INVALID', 'Unsafe fixture reference field', (0, fixture_document_1.fixtureErrorContext)(fixture, 'preparing references', path));
94
+ }
95
+ if (!dependencies.some((dependency) => dependency.name === name)) {
96
+ dependencies.push({ name: name, path });
97
+ }
98
+ return selected;
99
+ }
100
+ if (Array.isArray(value)) {
101
+ return value.map((item, index) => prepareValue(item, definitions, dependencies, random, fixture, (0, fixture_error_1.appendFixturePath)(path, index)));
102
+ }
103
+ if ((0, fixture_document_1.isFixtureRecord)(value)) {
104
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
105
+ key,
106
+ prepareValue(item, definitions, dependencies, random, fixture, (0, fixture_error_1.appendFixturePath)(path, key)),
107
+ ]));
108
+ }
109
+ return value;
110
+ }
111
+ function selectReference(value, definitions, random, fixture, path) {
112
+ const candidates = referenceCandidates(value, definitions, fixture, path);
113
+ return candidates ? `@${pick(candidates, random)}` : value;
114
+ }
115
+ function referenceCandidates(value, definitions, fixture, path, stage = 'preparing references') {
116
+ const wildcard = value.match(WILDCARD_REFERENCE);
117
+ if (wildcard) {
118
+ const pattern = new RegExp(`^${wildcard[1]}\\d+$`);
119
+ const candidates = [...definitions.keys()].filter((name) => pattern.test(name));
120
+ if (!candidates.length) {
121
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_MISSING', 'Fixture reference candidates not found', (0, fixture_document_1.fixtureErrorContext)(fixture, stage, path));
122
+ }
123
+ return candidates;
124
+ }
125
+ const range = value.match(RANGE_REFERENCE);
126
+ if (!range)
127
+ return undefined;
128
+ const start = Number(range[2]);
129
+ const end = Number(range[3]);
130
+ const length = end - start + 1;
131
+ if (!Number.isSafeInteger(start) ||
132
+ !Number.isSafeInteger(end) ||
133
+ !Number.isSafeInteger(length) ||
134
+ start > end) {
135
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_INVALID', 'Invalid fixture reference range', (0, fixture_document_1.fixtureErrorContext)(fixture, stage, path));
136
+ }
137
+ if (length > definitions.size) {
138
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_MISSING', 'Fixture reference candidates not found', (0, fixture_document_1.fixtureErrorContext)(fixture, stage, path));
139
+ }
140
+ const candidates = Array.from({ length }, (_, offset) => `${range[1]}${start + offset}`);
141
+ if (candidates.some((name) => !definitions.has(name))) {
142
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_MISSING', 'Fixture reference candidates not found', (0, fixture_document_1.fixtureErrorContext)(fixture, stage, path));
143
+ }
144
+ return candidates;
145
+ }
146
+ function pick(values, random) {
147
+ return values[Math.floor(random.next() * values.length)];
148
+ }
149
+ function orderFixtures(fixtures, dependencies, stage) {
150
+ const byName = new Map(fixtures.map((fixture) => [fixture.name, fixture]));
151
+ const state = new Map();
152
+ const ordered = [];
153
+ const stack = [];
154
+ const visit = (fixture) => {
155
+ if (state.get(fixture.name) === 'visited')
156
+ return;
157
+ state.set(fixture.name, 'visiting');
158
+ stack.push(fixture.name);
159
+ for (const dependency of dependencies.get(fixture.name) ?? []) {
160
+ if (state.get(dependency.name) === 'visiting') {
161
+ const start = stack.indexOf(dependency.name);
162
+ const cycle = [...stack.slice(start), dependency.name].join(' -> ');
163
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_DEPENDENCY_CYCLE', `Fixture dependency cycle: ${cycle}`, (0, fixture_document_1.fixtureErrorContext)(fixture, stage, dependency.path));
164
+ }
165
+ visit(byName.get(dependency.name));
166
+ }
167
+ stack.pop();
168
+ state.set(fixture.name, 'visited');
169
+ ordered.push(fixture);
170
+ };
171
+ fixtures.forEach(visit);
172
+ return ordered;
173
+ }
174
+ function resolveFixtureReferences(value, records, fixture, path = '') {
175
+ if (typeof value === 'string') {
176
+ if (value.startsWith('@@'))
177
+ return value.slice(1);
178
+ if (!value.startsWith('@'))
179
+ return value;
180
+ const match = value.match(FIXED_REFERENCE);
181
+ if (!match) {
182
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_INVALID', 'Invalid fixture reference', (0, fixture_document_1.fixtureErrorContext)(fixture, 'resolving references', path));
183
+ }
184
+ const [, name, field] = match;
185
+ const record = records[name];
186
+ if (!record) {
187
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_MISSING', 'Fixture reference target was not loaded', (0, fixture_document_1.fixtureErrorContext)(fixture, 'resolving references', path));
188
+ }
189
+ if (!field)
190
+ return record;
191
+ if (fixture_document_1.DANGEROUS_KEYS.has(field)) {
192
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_INVALID', 'Unsafe fixture reference field', (0, fixture_document_1.fixtureErrorContext)(fixture, 'resolving references', path));
193
+ }
194
+ if (!Object.hasOwn(record, field) || record[field] === undefined) {
195
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_REFERENCE_FIELD_MISSING', 'Referenced fixture field is missing from the loaded record', (0, fixture_document_1.fixtureErrorContext)(fixture, 'resolving references', path));
196
+ }
197
+ return record[field];
198
+ }
199
+ if (Array.isArray(value)) {
200
+ return value.map((item, index) => resolveFixtureReferences(item, records, fixture, (0, fixture_error_1.appendFixturePath)(path, index)));
201
+ }
202
+ if ((0, fixture_document_1.isFixtureRecord)(value)) {
203
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
204
+ key,
205
+ resolveFixtureReferences(item, records, fixture, (0, fixture_error_1.appendFixturePath)(path, key)),
206
+ ]));
207
+ }
208
+ return value;
209
+ }
210
+ function applyFixtureConnections(data, connectedFields, fixture) {
211
+ if (!connectedFields?.length)
212
+ return data;
213
+ const connected = { ...data };
214
+ for (const field of connectedFields) {
215
+ if (!Object.hasOwn(connected, field) || connected[field] == null)
216
+ continue;
217
+ const value = connected[field];
218
+ const fieldPath = (0, fixture_error_1.appendFixturePath)('', field);
219
+ connected[field] = {
220
+ connect: Array.isArray(value)
221
+ ? value.map((record, index) => connectionId(record, fixture, (0, fixture_error_1.appendFixturePath)(fieldPath, index)))
222
+ : connectionId(value, fixture, fieldPath),
223
+ };
224
+ }
225
+ return connected;
226
+ }
227
+ function connectionId(value, fixture, path) {
228
+ if (!(0, fixture_document_1.isFixtureRecord)(value) ||
229
+ !Object.hasOwn(value, 'id') ||
230
+ value.id === undefined) {
231
+ throw (0, fixture_error_1.createFixtureError)('FIXTURE_CONNECTION_FAILED', 'Fixture connection record is invalid', (0, fixture_document_1.fixtureErrorContext)(fixture, 'connecting fixture', path));
232
+ }
233
+ return { id: value.id };
234
+ }