@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,5 @@
1
+ export type FixturePreservedTable = string;
2
+ export type FixtureCleanupOptions = {
3
+ preserveTables?: FixturePreservedTable[];
4
+ };
5
+ export declare function normalizeCleanupOptions(value?: unknown): FixtureCleanupOptions;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeCleanupOptions = normalizeCleanupOptions;
4
+ const fixture_document_1 = require("./fixture-document");
5
+ function normalizeCleanupOptions(value = {}) {
6
+ if (!(0, fixture_document_1.isFixtureRecord)(value) ||
7
+ Object.keys(value).some((key) => key !== 'preserveTables')) {
8
+ throw new Error('Invalid fixture cleanup options');
9
+ }
10
+ const preserveTables = value.preserveTables;
11
+ if (preserveTables === undefined)
12
+ return {};
13
+ if (!Array.isArray(preserveTables) ||
14
+ preserveTables.some((entry) => typeof entry !== 'string' || !/^[^.]+\.[^.]+$/.test(entry)) ||
15
+ new Set(preserveTables).size !== preserveTables.length) {
16
+ throw new Error('Invalid fixture cleanup options');
17
+ }
18
+ return { preserveTables };
19
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_module_1 = require("node:module");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_url_1 = require("node:url");
11
+ const node_util_1 = require("node:util");
12
+ const ajv_1 = __importDefault(require("ajv"));
13
+ const fixture_document_1 = require("./fixture-document");
14
+ const fixture_error_1 = require("./fixture-error");
15
+ const fixture_reference_1 = require("./fixture-reference");
16
+ const load_options_1 = require("./load-options");
17
+ const prisma_config_1 = require("./prisma-config");
18
+ const fixture_config_1 = require("./fixture-config");
19
+ const index_1 = require("./index");
20
+ const help = `Usage: prisma-fixtures [path...] [--client <module>] [options]
21
+ prisma-fixtures init [fixtures-directory]
22
+
23
+ Load YAML/JSON fixtures using your Prisma client, in one transaction.
24
+ Defaults are read from .prisma-fixtures (JSON) and adjacent Prisma config.
25
+ A string client exports a client/factory; a config client object names generated Prisma output.
26
+ init creates a minimal config without overwriting existing config.
27
+
28
+ --config <file> Read a JSON config instead of .prisma-fixtures
29
+ --client <module> Path to your client module (.js, .cjs, .mjs, .ts)
30
+ --databaseUrl <url> Override Prisma config URL or pass to a client factory
31
+ --require <module> Preload a module; repeat for additional modules
32
+ --timeout <ms> Transaction timeout (default: 60000)
33
+ --seed <integer> Reproduce generated values and random references
34
+ --refDate <timestamp> Fix Faker relative dates (canonical UTC timestamp)
35
+ --clean Clear database data, preserving schema and migrations
36
+ --reset Clear database data, then load fixtures
37
+ --list List fixture names/entities without executing code
38
+ --lint Check fixture syntax/structure without executing code
39
+ --schema <file> Also validate --lint against a generated JSON Schema
40
+ --debug, -d Include error type/code, never record data or URLs
41
+ --no-color Accepted for compatibility; output is always plain
42
+ --version, -v Print the package version
43
+ --help, -h Show this help
44
+ `;
45
+ let stage = 'arguments';
46
+ let debug = false;
47
+ async function main() {
48
+ const { values, positionals } = parseCliArguments();
49
+ debug = values.debug ?? false;
50
+ const lint = values.lint ?? false;
51
+ if (values.help)
52
+ return void process.stdout.write(help);
53
+ if (values.version) {
54
+ const { version } = require('../package.json');
55
+ return void console.log(version);
56
+ }
57
+ if (positionals[0] === 'init') {
58
+ if (positionals.length > 2 || Object.keys(values).length !== 0) {
59
+ throw new Error('init accepts only an optional fixtures directory');
60
+ }
61
+ stage = 'initializing config';
62
+ return initializeConfig(positionals[1]);
63
+ }
64
+ stage = 'reading config';
65
+ const config = (0, fixture_config_1.readFixtureConfig)(values.config);
66
+ const targets = positionals.length ? positionals : (config.fixtures ?? []);
67
+ stage = 'arguments';
68
+ if ((!values.clean && targets.length === 0) ||
69
+ [values.list, lint, values.clean, values.reset].filter(Boolean).length > 1) {
70
+ process.stderr.write(help);
71
+ process.exitCode = 1;
72
+ return;
73
+ }
74
+ const timeout = Number(values.timeout ?? config.timeout ?? 60000);
75
+ if (!Number.isSafeInteger(timeout) || timeout <= 0) {
76
+ throw new Error('Invalid transaction timeout');
77
+ }
78
+ const cliSeed = values.seed;
79
+ if (cliSeed !== undefined && !/^\d+$/.test(cliSeed)) {
80
+ throw new Error('Invalid fixture load options');
81
+ }
82
+ const loadOptions = (0, load_options_1.normalizeLoadOptions)({
83
+ seed: cliSeed === undefined ? config.seed : Number(cliSeed),
84
+ refDate: values.refDate ?? config.refDate,
85
+ });
86
+ const schemaFile = values.schema ?? config.schema;
87
+ const validate = lint && schemaFile !== undefined
88
+ ? readSchemaValidator(schemaFile)
89
+ : undefined;
90
+ stage = 'reading fixtures';
91
+ const definitions = values.clean ? [] : (0, fixture_document_1.readFixturePaths)(targets, validate);
92
+ if (lint) {
93
+ if (definitions.length > fixture_document_1.MAX_FIXTURE_DEFINITIONS) {
94
+ throw new fixture_document_1.FixtureDocumentError('Too many fixture definitions');
95
+ }
96
+ const { unresolved } = (0, fixture_reference_1.lintFixtureReferences)(definitions);
97
+ console.log(`Linted ${definitions.length} fixtures (syntax and structure${schemaFile === undefined ? '' : ', model schema'}, static references; ${unresolved} dynamic values or multi-candidate references unresolved).`);
98
+ return;
99
+ }
100
+ if (values.list) {
101
+ console.log(JSON.stringify(definitions.map(({ name, entity }) => ({ name, entity })), null, 2));
102
+ return;
103
+ }
104
+ const { client, guard, preserveTables } = await loadClient(config, values);
105
+ let count;
106
+ let operationError;
107
+ try {
108
+ if (typeof client.$transaction !== 'function') {
109
+ throw new Error('The client must implement $transaction');
110
+ }
111
+ stage = values.clean
112
+ ? 'cleaning fixtures'
113
+ : values.reset
114
+ ? 'resetting fixtures'
115
+ : 'loading fixtures';
116
+ const action = async (transaction) => {
117
+ if (values.clean) {
118
+ await (0, index_1.cleanFixtures)(transaction, {
119
+ preserveTables,
120
+ });
121
+ return;
122
+ }
123
+ return values.reset
124
+ ? (0, index_1.resetFixtures)(transaction, definitions, {
125
+ preserveTables,
126
+ ...loadOptions,
127
+ })
128
+ : (0, index_1.loadFixtures)(transaction, definitions, loadOptions);
129
+ };
130
+ const records = guard
131
+ ? await guard.fixtureTransaction(client, action, timeout)
132
+ : await client.$transaction(action, { timeout });
133
+ count = records === undefined ? 0 : Object.keys(records).length;
134
+ }
135
+ catch (error) {
136
+ operationError = error;
137
+ throw error;
138
+ }
139
+ finally {
140
+ try {
141
+ if (operationError === undefined)
142
+ stage = 'disconnecting the client';
143
+ await client.$disconnect();
144
+ }
145
+ catch (error) {
146
+ if (operationError === undefined)
147
+ throw error;
148
+ }
149
+ }
150
+ console.log(values.clean
151
+ ? 'Cleaned database data.'
152
+ : `${values.reset ? 'Reset and loaded' : 'Loaded'} ${count} fixtures.`);
153
+ }
154
+ function parseCliArguments() {
155
+ return (0, node_util_1.parseArgs)({
156
+ allowPositionals: true,
157
+ options: {
158
+ config: { type: 'string' },
159
+ client: { type: 'string' },
160
+ databaseUrl: { type: 'string' },
161
+ require: { type: 'string', multiple: true },
162
+ timeout: { type: 'string' },
163
+ seed: { type: 'string' },
164
+ refDate: { type: 'string' },
165
+ clean: { type: 'boolean' },
166
+ reset: { type: 'boolean' },
167
+ list: { type: 'boolean' },
168
+ lint: { type: 'boolean' },
169
+ schema: { type: 'string' },
170
+ debug: { type: 'boolean', short: 'd' },
171
+ 'no-color': { type: 'boolean' },
172
+ version: { type: 'boolean', short: 'v' },
173
+ help: { type: 'boolean', short: 'h' },
174
+ },
175
+ });
176
+ }
177
+ function initializeConfig(directory) {
178
+ if (node_fs_1.default.existsSync('.prisma-fixtures')) {
179
+ console.error('.prisma-fixtures already exists; no changes made.');
180
+ process.exitCode = 1;
181
+ return;
182
+ }
183
+ const fixtures = directory ??
184
+ (node_fs_1.default.existsSync('prisma/fixtures') ? './prisma/fixtures' : './fixtures');
185
+ if (directory) {
186
+ if (!node_fs_1.default.existsSync(fixtures) || !node_fs_1.default.statSync(fixtures).isDirectory()) {
187
+ console.error('Fixture directory does not exist; no changes made.');
188
+ process.exitCode = 1;
189
+ return;
190
+ }
191
+ }
192
+ else {
193
+ node_fs_1.default.mkdirSync(fixtures, { recursive: true });
194
+ }
195
+ node_fs_1.default.writeFileSync('.prisma-fixtures', `${JSON.stringify({ fixtures: [fixtures] }, null, 2)}\n`, { flag: 'wx' });
196
+ console.log(`Created .prisma-fixtures using ${fixtures}.`);
197
+ }
198
+ function readSchemaValidator(schemaFile) {
199
+ stage = 'reading schema';
200
+ const raw = node_fs_1.default.readFileSync(node_path_1.default.resolve(schemaFile), 'utf8');
201
+ let validate;
202
+ try {
203
+ validate = new ajv_1.default({ strict: false, validateFormats: false }).compile(JSON.parse(raw));
204
+ }
205
+ catch {
206
+ throw new fixture_document_1.FixtureDocumentError('Invalid fixture JSON Schema');
207
+ }
208
+ return (document, file) => {
209
+ if (validate(document))
210
+ return;
211
+ const error = validate.errors?.[0];
212
+ throw new fixture_document_1.FixtureDocumentError(`Fixture document ${JSON.stringify(node_path_1.default.basename(file))}: ${JSON.stringify(error?.instancePath || '/')} ${error?.message ?? 'does not match the schema'}`);
213
+ };
214
+ }
215
+ async function loadClient(config, values) {
216
+ stage = 'loading the client';
217
+ let clientConfig = values.client ? node_path_1.default.resolve(values.client) : config.client;
218
+ const requireFromCwd = (0, node_module_1.createRequire)(node_path_1.default.resolve('package.json'));
219
+ for (const preload of values.require ?? [])
220
+ requireFromCwd(preload);
221
+ let prismaDefaults;
222
+ if (!clientConfig) {
223
+ stage = 'loading Prisma config';
224
+ prismaDefaults = await (0, prisma_config_1.loadPrismaDefaults)(node_path_1.default.dirname(node_path_1.default.resolve(values.config ?? '.prisma-fixtures')), requireFromCwd, values.databaseUrl);
225
+ const clientExtension = node_path_1.default.extname(prismaDefaults.module);
226
+ if ((clientExtension === '.ts' || clientExtension === '.cts') &&
227
+ !require.extensions[clientExtension]) {
228
+ requireFromCwd('ts-node/register');
229
+ }
230
+ clientConfig = { module: prismaDefaults.module, adapter: 'pg' };
231
+ stage = 'loading the client';
232
+ }
233
+ const preserveTables = [
234
+ ...new Set([
235
+ ...(config.preserveTables ?? []),
236
+ ...(prismaDefaults?.preserveTables ?? []),
237
+ ]),
238
+ ];
239
+ let candidate;
240
+ let guard;
241
+ if (typeof clientConfig === 'string') {
242
+ const imported = await import((0, node_url_1.pathToFileURL)(clientConfig).href);
243
+ let exported = imported.default;
244
+ if (exported && typeof exported === 'object' && 'default' in exported) {
245
+ exported = exported.default;
246
+ }
247
+ if (values.databaseUrl !== undefined && typeof exported !== 'function') {
248
+ throw new Error('--databaseUrl requires a client factory');
249
+ }
250
+ candidate =
251
+ typeof exported === 'function'
252
+ ? await exported({ databaseUrl: values.databaseUrl })
253
+ : exported;
254
+ }
255
+ else {
256
+ if (clientConfig.guard) {
257
+ const imported = requireFromCwd(clientConfig.guard);
258
+ if (!(0, fixture_document_1.isFixtureRecord)(imported) ||
259
+ typeof imported.fixtureDatabaseUrl !== 'function' ||
260
+ typeof imported.fixtureTransaction !== 'function') {
261
+ throw new Error('Invalid fixture guard module');
262
+ }
263
+ guard = imported;
264
+ }
265
+ const environment = {
266
+ ...process.env,
267
+ ...(values.databaseUrl === undefined
268
+ ? {}
269
+ : { DATABASE_URL: values.databaseUrl }),
270
+ };
271
+ const databaseUrl = guard
272
+ ? guard.fixtureDatabaseUrl(environment)
273
+ : (values.databaseUrl ??
274
+ prismaDefaults?.databaseUrl ??
275
+ environment.DATABASE_URL);
276
+ if (typeof databaseUrl !== 'string' || !databaseUrl.trim()) {
277
+ throw new Error('DATABASE_URL is required');
278
+ }
279
+ const generated = await importGeneratedClient(clientConfig.module, requireFromCwd);
280
+ candidate = createGeneratedClient(generated, requireFromCwd, databaseUrl);
281
+ }
282
+ if (!candidate ||
283
+ typeof candidate !== 'object' ||
284
+ !('$disconnect' in candidate) ||
285
+ typeof candidate.$disconnect !== 'function') {
286
+ throw new Error('The client must implement $disconnect');
287
+ }
288
+ return { client: candidate, guard, preserveTables };
289
+ }
290
+ function createGeneratedClient(generated, requireFromCwd, databaseUrl) {
291
+ const clientExports = (0, fixture_document_1.isFixtureRecord)(generated) ? generated : undefined;
292
+ const defaultExport = (0, fixture_document_1.isFixtureRecord)(clientExports?.default)
293
+ ? clientExports.default
294
+ : undefined;
295
+ const PrismaClient = clientExports?.PrismaClient ?? defaultExport?.PrismaClient;
296
+ const adapter = requireFromCwd('@prisma/adapter-pg');
297
+ if (typeof PrismaClient !== 'function' ||
298
+ typeof adapter.PrismaPg !== 'function') {
299
+ throw new Error('Invalid generated Prisma client or pg adapter');
300
+ }
301
+ return new PrismaClient({
302
+ adapter: new adapter.PrismaPg({ connectionString: databaseUrl }),
303
+ });
304
+ }
305
+ async function importGeneratedClient(modulePath, requireFromCwd) {
306
+ try {
307
+ return requireFromCwd(modulePath);
308
+ }
309
+ catch (error) {
310
+ if (error === null ||
311
+ typeof error !== 'object' ||
312
+ !('code' in error) ||
313
+ !['ERR_REQUIRE_ESM', 'ERR_REQUIRE_ASYNC_MODULE'].includes(String(error.code))) {
314
+ throw error;
315
+ }
316
+ return import((0, node_url_1.pathToFileURL)(modulePath).href);
317
+ }
318
+ }
319
+ main().catch((error) => {
320
+ if ((0, fixture_error_1.isTrustedFixtureError)(error)) {
321
+ console.error(formatFixtureError(error));
322
+ process.exitCode = 1;
323
+ return;
324
+ }
325
+ // Prisma and user hooks can put database URLs or record data in messages/stacks.
326
+ const details = debug && error instanceof Error
327
+ ? ` (${error.name.replace(/[^A-Za-z0-9_]/g, '').slice(0, 60)})`
328
+ : '';
329
+ console.error(`Fixture command failed during ${stage}${details}.`);
330
+ process.exitCode = 1;
331
+ });
332
+ function formatFixtureError(error) {
333
+ const location = [error.file, error.fixtureName, error.path]
334
+ .filter((value) => value !== undefined)
335
+ .map((value) => JSON.stringify(value))
336
+ .join(' ');
337
+ const output = `Fixture command failed during ${error.stage} [${error.code}]: ${location}${location ? ' ' : ''}${error.message}.`;
338
+ const suffix = '… [truncated]';
339
+ return output.length <= 4096
340
+ ? output
341
+ : `${output.slice(0, 4096 - suffix.length)}${suffix}`;
342
+ }
@@ -0,0 +1,14 @@
1
+ import { type FixtureCleanupOptions } from './cleanup-options';
2
+ import { type FixtureLoadOptions } from './load-options';
3
+ export type ConfiguredPrismaClient = {
4
+ module: string;
5
+ adapter: 'pg';
6
+ guard?: string;
7
+ };
8
+ export type FixtureConfig = FixtureCleanupOptions & FixtureLoadOptions & {
9
+ fixtures?: string[];
10
+ client?: string | ConfiguredPrismaClient;
11
+ timeout?: number;
12
+ schema?: string;
13
+ };
14
+ export declare function readFixtureConfig(file?: string): FixtureConfig;
@@ -0,0 +1,95 @@
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.readFixtureConfig = readFixtureConfig;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const cleanup_options_1 = require("./cleanup-options");
10
+ const fixture_document_1 = require("./fixture-document");
11
+ const load_options_1 = require("./load-options");
12
+ function readFixtureConfig(file) {
13
+ const configFile = node_path_1.default.resolve(file ?? '.prisma-fixtures');
14
+ let raw;
15
+ try {
16
+ raw = node_fs_1.default.readFileSync(configFile, 'utf8');
17
+ }
18
+ catch (error) {
19
+ if (file === undefined &&
20
+ error instanceof Error &&
21
+ 'code' in error &&
22
+ error.code === 'ENOENT') {
23
+ return {};
24
+ }
25
+ throw error;
26
+ }
27
+ const config = JSON.parse(raw);
28
+ if (!(0, fixture_document_1.isFixtureRecord)(config) ||
29
+ Object.keys(config).some((key) => ![
30
+ 'fixtures',
31
+ 'client',
32
+ 'timeout',
33
+ 'schema',
34
+ 'preserveTables',
35
+ 'seed',
36
+ 'refDate',
37
+ ].includes(key))) {
38
+ throw new Error('Invalid fixture CLI config');
39
+ }
40
+ const { fixtures, client, timeout, schema, preserveTables, seed, refDate } = config;
41
+ if ((fixtures !== undefined &&
42
+ (!Array.isArray(fixtures) ||
43
+ !fixtures.length ||
44
+ !fixtures.every(isPath))) ||
45
+ (client !== undefined && !isClientConfig(client)) ||
46
+ (schema !== undefined && !isPath(schema)) ||
47
+ (timeout !== undefined &&
48
+ (typeof timeout !== 'number' ||
49
+ !Number.isSafeInteger(timeout) ||
50
+ timeout <= 0))) {
51
+ throw new Error('Invalid fixture CLI config');
52
+ }
53
+ let cleanup;
54
+ let load;
55
+ try {
56
+ cleanup = (0, cleanup_options_1.normalizeCleanupOptions)({ preserveTables });
57
+ load = (0, load_options_1.normalizeLoadOptions)({ seed, refDate });
58
+ }
59
+ catch {
60
+ throw new Error('Invalid fixture CLI config');
61
+ }
62
+ const directory = node_path_1.default.dirname(configFile);
63
+ return {
64
+ fixtures: fixtures?.map((target) => node_path_1.default.resolve(directory, target)),
65
+ client: resolveClientPaths(client, directory),
66
+ timeout,
67
+ schema: schema === undefined ? undefined : node_path_1.default.resolve(directory, schema),
68
+ ...cleanup,
69
+ ...load,
70
+ };
71
+ }
72
+ function isPath(value) {
73
+ return typeof value === 'string' && value.trim().length > 0;
74
+ }
75
+ function isClientConfig(value) {
76
+ return (isPath(value) ||
77
+ ((0, fixture_document_1.isFixtureRecord)(value) &&
78
+ Object.keys(value).every((key) => ['module', 'adapter', 'guard'].includes(key)) &&
79
+ isPath(value.module) &&
80
+ value.adapter === 'pg' &&
81
+ (value.guard === undefined || isPath(value.guard))));
82
+ }
83
+ function resolveClientPaths(client, directory) {
84
+ if (client === undefined)
85
+ return undefined;
86
+ if (typeof client === 'string')
87
+ return node_path_1.default.resolve(directory, client);
88
+ return {
89
+ module: node_path_1.default.resolve(directory, client.module),
90
+ adapter: 'pg',
91
+ ...(client.guard === undefined
92
+ ? {}
93
+ : { guard: node_path_1.default.resolve(directory, client.guard) }),
94
+ };
95
+ }
@@ -0,0 +1,23 @@
1
+ import { FixtureError, type FixtureErrorContext } from './fixture-error';
2
+ export declare class FixtureDocumentError extends FixtureError {
3
+ constructor(message: string, context?: Partial<FixtureErrorContext>, cause?: unknown);
4
+ }
5
+ export type FixtureDefinition = {
6
+ name: string;
7
+ entity: string;
8
+ data: Record<string, unknown>;
9
+ parameters: Record<string, unknown>;
10
+ processor?: string;
11
+ locale?: string;
12
+ connectedFields?: string[];
13
+ deferredFields?: string[];
14
+ };
15
+ export declare function fixtureErrorContext(fixture: FixtureDefinition | undefined, stage: string, path?: string): FixtureErrorContext;
16
+ export declare function inheritFixtureSource(source: FixtureDefinition, target: FixtureDefinition): void;
17
+ export declare const MAX_FIXTURE_DEFINITIONS = 2000;
18
+ export declare const DANGEROUS_KEYS: Set<string>;
19
+ export declare function readFixturePaths(targets: string[], validateDocument?: (value: unknown, file: string) => void): FixtureDefinition[];
20
+ export declare function readFixtureDocuments(targetPath: string, validateDocument?: (value: unknown, file: string) => void): FixtureDefinition[];
21
+ export declare function assertFixtureDefinition(value: unknown): asserts value is FixtureDefinition;
22
+ export declare function isFixtureRecord(value: unknown): value is Record<string, unknown>;
23
+ export declare function assertSafeFixtureValue(value: unknown): void;