@prisma-next/cli 0.7.0 → 0.8.0

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,130 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import type { PackageManager } from './detect-package-manager';
4
+ import { errorInitSkillInstallFailed } from './errors';
5
+
6
+ const exec = promisify(execFile);
7
+
8
+ /**
9
+ * The npm package the agent-skill install dispatches to. Version-locked
10
+ * to Prisma Next via the consumer project's `package.json`; the install
11
+ * subprocess picks up whatever version is resolvable at install time.
12
+ *
13
+ * The skill is **always** installed at the project level — never the
14
+ * user level — precisely so the skill version tracks the project's
15
+ * Prisma Next version. A user-level (global) install of an
16
+ * agent-skills CLI package would have to pick a single version for
17
+ * every project on the host, which breaks the version-locking invariant
18
+ * the rest of the framework relies on (skills, CLI, runtime, and
19
+ * extension packs all ship at the same version per release).
20
+ */
21
+ export const AGENT_SKILL_PACKAGE = '@prisma-next/agent-skill';
22
+
23
+ /**
24
+ * The skill-install command, formatted for the project's detected
25
+ * package manager. `npx`/`pnpm dlx`/`bunx` are interchangeable to the
26
+ * user; we pick the variant that matches the rest of the install step
27
+ * so a single project consistently uses one runner.
28
+ *
29
+ * `--all` auto-selects every skill in the cluster and every detected
30
+ * agent runtime, skipping the multi-select prompts the `skills` CLI
31
+ * shows by default. A non-interactive scaffold step cannot present
32
+ * prompts, and the cluster is designed to be installed as a unit (the
33
+ * router skill routes between the workflow-scoped siblings). Users who
34
+ * want a narrower install run `npx skills add @prisma-next/agent-skill`
35
+ * themselves after `init` with the flags they want.
36
+ *
37
+ * Exported for unit tests so the per-PM dispatch can be asserted
38
+ * without a live subprocess.
39
+ */
40
+ export function formatSkillInstallCommand(pm: PackageManager): string {
41
+ const args = ['skills', 'add', AGENT_SKILL_PACKAGE, '--all'];
42
+ switch (pm) {
43
+ case 'pnpm':
44
+ return `pnpm dlx ${args.join(' ')}`;
45
+ case 'yarn':
46
+ return `yarn dlx ${args.join(' ')}`;
47
+ case 'bun':
48
+ return `bunx ${args.join(' ')}`;
49
+ case 'deno':
50
+ return `deno run -A npm:${args.join(' ')}`;
51
+ case 'npm':
52
+ return `npx ${args.join(' ')}`;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Parse the project-pm-formatted command into an exec call. The
58
+ * format-then-parse split keeps the user-facing command string the same
59
+ * as the surface the structured error advertises, so a user who copies
60
+ * the error's `fix` line gets the same invocation that init just
61
+ * attempted.
62
+ */
63
+ function commandToExec(command: string): {
64
+ readonly file: string;
65
+ readonly args: readonly string[];
66
+ } {
67
+ const tokens = command.split(/\s+/);
68
+ return { file: tokens[0] ?? 'npx', args: tokens.slice(1) };
69
+ }
70
+
71
+ /**
72
+ * Runs the project-level skill install. Returns `{ ok: true, command }`
73
+ * on success; throws a structured `errorInitSkillInstallFailed` on
74
+ * failure. The throw is intentionally fatal — project-level skill
75
+ * install is unconditional (modulo `--no-skill`) and the user opted
76
+ * into Prisma Next by running `init`. A silent skip would defeat the
77
+ * onboarding-to-zero contract.
78
+ */
79
+ export async function runProjectLevelSkillInstall(ctx: {
80
+ readonly baseDir: string;
81
+ readonly pm: PackageManager;
82
+ readonly filesWritten: readonly string[];
83
+ }): Promise<{ readonly ok: true; readonly command: string }> {
84
+ const command = formatSkillInstallCommand(ctx.pm);
85
+ const { file, args } = commandToExec(command);
86
+ try {
87
+ await exec(file, args, { cwd: ctx.baseDir });
88
+ return { ok: true, command };
89
+ } catch (err) {
90
+ throw errorInitSkillInstallFailed({
91
+ skillInstallCommand: command,
92
+ filesWritten: ctx.filesWritten,
93
+ cause:
94
+ redactSecrets(readChildStderr(err)) || (err instanceof Error ? err.message : String(err)),
95
+ });
96
+ }
97
+ }
98
+
99
+ function readChildStderr(err: unknown): string {
100
+ if (err instanceof Error && 'stderr' in err) {
101
+ return String((err as { stderr: string }).stderr ?? '');
102
+ }
103
+ return '';
104
+ }
105
+
106
+ /**
107
+ * Strips credentials from a `scheme://user:pass@host/...` URL anywhere
108
+ * in `stderr`. Package-manager stderr regularly contains credentialed
109
+ * registry URLs (private npm registries, GitHub Packages tokens), and
110
+ * those bubble into the structured `errorInitSkillInstallFailed`
111
+ * envelope, which ends up in logs and CI output. Redact at the
112
+ * boundary so we never re-emit a secret.
113
+ *
114
+ * Exported for unit tests.
115
+ */
116
+ export function redactSecrets(stderr: string): string {
117
+ if (!stderr) return stderr;
118
+ return stderr.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^/@\s]+)@/g, '$1***@');
119
+ }
120
+
121
+ // -------------------------------------------------------------------
122
+ // Legacy file cleanup
123
+ // -------------------------------------------------------------------
124
+
125
+ /**
126
+ * Hand-rolled skill stub path that init must not leave behind. Removed
127
+ * on every init run so a project's `.agents/skills/prisma-next/` does
128
+ * not shadow the published `@prisma-next/agent-skill` package.
129
+ */
130
+ export const LEGACY_SKILL_FILE = '.agents/skills/prisma-next/SKILL.md';
@@ -236,3 +236,35 @@ export function errorInitEmitFailed(options: {
236
236
  },
237
237
  });
238
238
  }
239
+
240
+ /**
241
+ * The project-level agent-skill install (`npx skills add
242
+ * @prisma-next/agent-skill`) failed after a successful dependency
243
+ * install + emit. The project's scaffold remains on disk; the user
244
+ * can either fix the underlying issue (network, registry, PATH) and
245
+ * run the install command manually, or re-run `init --no-skill` to
246
+ * proceed without the skill.
247
+ *
248
+ * Non-rolling-back, matching the existing install/emit failure
249
+ * semantics. Maps to exit code `6 = SKILL_INSTALL_FAILED`.
250
+ */
251
+ export function errorInitSkillInstallFailed(options: {
252
+ readonly skillInstallCommand: string;
253
+ readonly filesWritten: readonly string[];
254
+ readonly cause: string;
255
+ }): CliStructuredError {
256
+ return new CliStructuredError('5013', 'Failed to install @prisma-next/agent-skill', {
257
+ domain: 'CLI',
258
+ why: `\`${options.skillInstallCommand}\` exited with an error: ${options.cause}`,
259
+ fix:
260
+ 'Either:\n' +
261
+ ` - Re-run \`prisma-next init --no-skill${options.filesWritten.length > 0 ? ' --force' : ''}\` to skip the skill install for this run, or\n` +
262
+ ` - Fix the underlying issue (network, npm registry, \`npx skills\` on PATH) and install manually:\n ${options.skillInstallCommand}`,
263
+ docsUrl: 'https://prisma-next.dev/docs/cli/init#agent-skill',
264
+ meta: {
265
+ filesWritten: options.filesWritten,
266
+ skillInstallCommand: options.skillInstallCommand,
267
+ cause: options.cause,
268
+ },
269
+ });
270
+ }
@@ -60,3 +60,13 @@ export const INIT_EXIT_INSTALL_FAILED = 4;
60
60
  * emit` manually.
61
61
  */
62
62
  export const INIT_EXIT_EMIT_FAILED = 5;
63
+
64
+ /**
65
+ * The project-level `@prisma-next/agent-skill` install (`npx skills add
66
+ * @prisma-next/agent-skill`) failed after a successful dependency
67
+ * install + emit. The scaffolded project files remain on disk; the
68
+ * user can fix the underlying issue (network, registry reachability,
69
+ * `npx skills` not on PATH) and run the install manually, or re-run
70
+ * `init` with `--no-skill` to skip it.
71
+ */
72
+ export const INIT_EXIT_SKILL_INSTALL_FAILED = 6;
@@ -11,6 +11,7 @@ import {
11
11
  INIT_EXIT_INTERNAL_ERROR,
12
12
  INIT_EXIT_OK,
13
13
  INIT_EXIT_PRECONDITION,
14
+ INIT_EXIT_SKILL_INSTALL_FAILED,
14
15
  INIT_EXIT_USER_ABORTED,
15
16
  } from './exit-codes';
16
17
 
@@ -33,6 +34,7 @@ interface InitCommandOptions extends CommonCommandOptions {
33
34
  readonly probeDb?: boolean;
34
35
  readonly strictProbe?: boolean;
35
36
  readonly install?: boolean;
37
+ readonly skill?: boolean;
36
38
  }
37
39
 
38
40
  export function createInitCommand(): Command {
@@ -52,7 +54,8 @@ export function createInitCommand(): Command {
52
54
  ` ${INIT_EXIT_PRECONDITION} PRECONDITION Bad flags / missing prerequisite (e.g. no package.json).\n` +
53
55
  ` ${INIT_EXIT_USER_ABORTED} USER_ABORTED User cancelled an interactive prompt.\n` +
54
56
  ` ${INIT_EXIT_INSTALL_FAILED} INSTALL_FAILED Dependency installation failed (init-specific).\n` +
55
- ` ${INIT_EXIT_EMIT_FAILED} EMIT_FAILED \`contract emit\` failed after install (init-specific).`,
57
+ ` ${INIT_EXIT_EMIT_FAILED} EMIT_FAILED \`contract emit\` failed after install (init-specific).\n` +
58
+ ` ${INIT_EXIT_SKILL_INSTALL_FAILED} SKILL_INSTALL_FAILED Agent-skill install failed (re-run with --no-skill to skip).`,
56
59
  );
57
60
  setCommandExamples(command, [
58
61
  'prisma-next init',
@@ -60,6 +63,7 @@ export function createInitCommand(): Command {
60
63
  'prisma-next init --yes --target mongodb --authoring typescript --json',
61
64
  'prisma-next init --yes --force --target postgres --authoring psl # overwrite an existing scaffold',
62
65
  'prisma-next init --no-install # skip pnpm/npm install + emit',
66
+ 'prisma-next init --no-skill # skip the agent-skill install (air-gapped / restricted env)',
63
67
  ]);
64
68
 
65
69
  return addGlobalOptions(command)
@@ -83,6 +87,10 @@ export function createInitCommand(): Command {
83
87
  'Treat a failed --probe-db as fatal (no-op without --probe-db; init is offline-by-default)',
84
88
  )
85
89
  .option('--no-install', 'Skip dependency installation and contract emission')
90
+ .option(
91
+ '--no-skill',
92
+ 'Skip the @prisma-next/agent-skill install (air-gapped CI, restricted registries, etc.)',
93
+ )
86
94
  .action(async (options: InitCommandOptions) => {
87
95
  const { runInit } = await import('./init');
88
96
  const flags = parseGlobalFlags(options);
@@ -7,6 +7,11 @@ import { CliStructuredError } from '../../utils/cli-errors';
7
7
  import { formatErrorJson, formatErrorOutput } from '../../utils/formatters/errors';
8
8
  import type { GlobalFlags } from '../../utils/global-flags';
9
9
  import { TerminalUI } from '../../utils/terminal-ui';
10
+ import {
11
+ formatSkillInstallCommand,
12
+ LEGACY_SKILL_FILE,
13
+ runProjectLevelSkillInstall,
14
+ } from './agent-skill-install';
10
15
  import {
11
16
  detectPackageManager,
12
17
  formatAddArgs,
@@ -29,6 +34,7 @@ import {
29
34
  INIT_EXIT_INTERNAL_ERROR,
30
35
  INIT_EXIT_OK,
31
36
  INIT_EXIT_PRECONDITION,
37
+ INIT_EXIT_SKILL_INSTALL_FAILED,
32
38
  INIT_EXIT_USER_ABORTED,
33
39
  } from './exit-codes';
34
40
  import { mergeGitattributes, requiredGitattributesLines } from './hygiene-gitattributes';
@@ -48,7 +54,6 @@ import {
48
54
  } from './output';
49
55
  import { type ProbeOutcome, type ProbeOverrides, probeServerVersion } from './probe-db';
50
56
  import { findStaleArtefacts, removeDependency } from './reinit-cleanup';
51
- import { agentSkillMd } from './templates/agent-skill';
52
57
  import { configFile, dbFile, starterSchema, targetPackageName } from './templates/code-templates';
53
58
  import { envExampleContent, envFileContent, MIN_SERVER_VERSION } from './templates/env';
54
59
  import { quickReferenceMd } from './templates/quick-reference';
@@ -72,6 +77,15 @@ interface InstallReport {
72
77
  readonly deps: readonly string[];
73
78
  readonly devDeps: readonly string[];
74
79
  readonly warnings: readonly string[];
80
+ /**
81
+ * The package manager that actually ran. Equal to the detected `pm`
82
+ * on the common path; differs when the FR7.2 pnpm → npm fallback
83
+ * fired, in which case it's `'npm'`. Threaded into the agent-skill
84
+ * install so the runner stays consistent with the install we just
85
+ * ran — re-trying through `pnpm dlx` when `pnpm install` just failed
86
+ * for workspace/catalog reasons would fail again for the same reason.
87
+ */
88
+ readonly effectivePm: PackageManager;
75
89
  }
76
90
 
77
91
  /**
@@ -156,10 +170,6 @@ export async function runInit(
156
170
  path: 'prisma-next.md',
157
171
  content: quickReferenceMd(inputs.target, inputs.authoring, inputs.schemaPath, pkgRun),
158
172
  },
159
- {
160
- path: '.agents/skills/prisma-next/SKILL.md',
161
- content: agentSkillMd(inputs.target, inputs.authoring, inputs.schemaPath, pkgRun),
162
- },
163
173
  { path: '.env.example', content: envExampleContent(inputs.target) },
164
174
  ];
165
175
 
@@ -172,6 +182,14 @@ export async function runInit(
172
182
  // and missing-on-disk-at-write-time is tolerated.
173
183
  const filesToDelete: string[] = inputs.reinit ? [...findStaleArtefacts(baseDir, schemaDir)] : [];
174
184
 
185
+ // `init` delegates the skill to `npx skills add @prisma-next/agent-skill`,
186
+ // so a hand-rolled `.agents/skills/prisma-next/SKILL.md` in the project
187
+ // would shadow the published package. Queue it for deletion on every
188
+ // run (not gated on `--reinit`).
189
+ if (existsSync(join(baseDir, LEGACY_SKILL_FILE))) {
190
+ filesToDelete.push(LEGACY_SKILL_FILE);
191
+ }
192
+
175
193
  // FR3.2: a real `.env` is only written when the user opted in. Never
176
194
  // overwrite an existing `.env` — secrets live there and clobbering
177
195
  // them is the most damaging possible side-effect of `init`.
@@ -416,6 +434,51 @@ export async function runInit(
416
434
  }
417
435
  }
418
436
 
437
+ // Agent-skill install. Project-level is unconditional modulo
438
+ // `--no-skill`. We deliberately do **not** offer a user-level
439
+ // (global) install path: the skill's behaviour and surface track
440
+ // the project's Prisma Next version, and a host-wide install would
441
+ // have to pick a single version for every project on the machine,
442
+ // which breaks the version-locking invariant the rest of the
443
+ // framework relies on. A project-level failure is fatal
444
+ // (`INIT_EXIT_SKILL_INSTALL_FAILED`).
445
+ //
446
+ // Runs after install + emit because (1) `node_modules` is in place,
447
+ // so any consumers downstream are coherent, and (2) the user has
448
+ // seen the scaffolding succeed before this network-bound step
449
+ // potentially fails. We skip the install when the user passed
450
+ // `--no-install` for the same reason — no `node_modules` means the
451
+ // workspace isn't ready to consume the skill yet anyway.
452
+ const manualProjectSkillCommand = formatSkillInstallCommand(install.effectivePm);
453
+ if (!inputs.installProjectSkill) {
454
+ warnings.push(
455
+ `Skipped @prisma-next/agent-skill install (--no-skill). To install later, run \`${manualProjectSkillCommand}\` in this project.`,
456
+ );
457
+ } else if (install.skipped) {
458
+ warnings.push(
459
+ `Skipped @prisma-next/agent-skill install because --no-install was passed. Once you run install manually, register the skill with \`${manualProjectSkillCommand}\`.`,
460
+ );
461
+ } else {
462
+ const spinner = ui.spinner();
463
+ spinner.start('Registering @prisma-next/agent-skill with the agent runtime...');
464
+ try {
465
+ const project = await runProjectLevelSkillInstall({
466
+ baseDir,
467
+ pm: install.effectivePm,
468
+ filesWritten,
469
+ });
470
+ spinner.stop(
471
+ `Registered @prisma-next/agent-skill (project level) — ran \`${project.command}\``,
472
+ );
473
+ } catch (error) {
474
+ spinner.stop('Agent-skill install failed');
475
+ if (CliStructuredError.is(error)) {
476
+ return emitError(ui, flags, error);
477
+ }
478
+ throw error;
479
+ }
480
+ }
481
+
419
482
  const output: InitOutput = {
420
483
  ok: true,
421
484
  target: inputs.target === 'mongo' ? 'mongodb' : 'postgres',
@@ -525,6 +588,8 @@ export function exitCodeForError(error: { readonly code: string }): number {
525
588
  return INIT_EXIT_EMIT_FAILED;
526
589
  case '5009': // invalid output document — internal bug in prisma-next
527
590
  return INIT_EXIT_INTERNAL_ERROR;
591
+ case '5013': // agent-skill install failed
592
+ return INIT_EXIT_SKILL_INSTALL_FAILED;
528
593
  default:
529
594
  // Any unexpected code is treated as an internal bug rather than
530
595
  // mis-routed to PRECONDITION. Adding a new code requires an
@@ -646,7 +711,7 @@ async function runInstall(ctx: {
646
711
  'Manual steps',
647
712
  );
648
713
  }
649
- return { skipped: true, deps: [], devDeps: [], warnings: catalogWarnings };
714
+ return { skipped: true, deps: [], devDeps: [], warnings: catalogWarnings, effectivePm: pm };
650
715
  }
651
716
 
652
717
  const exec = promisify(execFile);
@@ -661,7 +726,7 @@ async function runInstall(ctx: {
661
726
  try {
662
727
  await runPair(pm);
663
728
  spinner.stop(`Installed ${allPackages}`);
664
- return { skipped: false, deps, devDeps, warnings: catalogWarnings };
729
+ return { skipped: false, deps, devDeps, warnings: catalogWarnings, effectivePm: pm };
665
730
  } catch (err) {
666
731
  const stderrText = redactSecrets(readChildStderr(err));
667
732
 
@@ -693,6 +758,7 @@ async function runInstall(ctx: {
693
758
  // catalog-honour warning to avoid a contradictory message
694
759
  // pair.
695
760
  warnings: [fallbackWarning],
761
+ effectivePm: 'npm',
696
762
  };
697
763
  } catch (npmErr) {
698
764
  spinner.stop('Installation failed');
@@ -31,6 +31,13 @@ export interface InitFlagOptions {
31
31
  readonly probeDb?: boolean;
32
32
  readonly strictProbe?: boolean;
33
33
  readonly install?: boolean;
34
+ /**
35
+ * `--no-skill` — skip the project-level skill install entirely.
36
+ * Documented escape hatch for air-gapped CI, restricted registries,
37
+ * and any environment where `npx skills` is
38
+ * not reachable.
39
+ */
40
+ readonly skill?: boolean;
34
41
  }
35
42
 
36
43
  /**
@@ -61,6 +68,14 @@ export interface ResolvedInitInputs {
61
68
  * is added separately via the install step.
62
69
  */
63
70
  readonly removePreviousFacade: string | null;
71
+ /**
72
+ * Whether to run `npx skills add @prisma-next/agent-skill` at the
73
+ * project level after install + emit. True by default; `--no-skill`
74
+ * sets it to `false`. The skill is always project-level (never
75
+ * user-level / global) so its version stays locked to the project's
76
+ * Prisma Next version — see `agent-skill-install.ts`.
77
+ */
78
+ readonly installProjectSkill: boolean;
64
79
  }
65
80
 
66
81
  const TARGET_ALIASES: ReadonlyMap<string, TargetId> = new Map([
@@ -161,6 +176,13 @@ export async function resolveInitInputs(ctx: {
161
176
  autoAcceptPrompts,
162
177
  });
163
178
 
179
+ // Skill-install gating. `--no-skill` (commander parses
180
+ // `options.skill === false`) is the only escape hatch; otherwise
181
+ // project-level install is unconditional. The skill is always
182
+ // installed at the project level so its version tracks the
183
+ // project's Prisma Next release.
184
+ const installProjectSkill = options.skill !== false;
185
+
164
186
  return {
165
187
  target: finalTarget,
166
188
  authoring: finalAuthoring,
@@ -171,6 +193,7 @@ export async function resolveInitInputs(ctx: {
171
193
  strictProbe: Boolean(options.strictProbe),
172
194
  reinit,
173
195
  removePreviousFacade,
196
+ installProjectSkill,
174
197
  };
175
198
  }
176
199
 
@@ -130,7 +130,7 @@ export function buildNextSteps(options: {
130
130
  '4. Open prisma-next.md for a quick reference on how to write your first typed query.',
131
131
  );
132
132
  steps.push(
133
- '5. The .agents/skills/prisma-next/SKILL.md file is wired up for AI-coding agents in this project.',
133
+ '5. @prisma-next/agent-skill is registered with your agent runtime — open the project in your IDE and ask the agent to add a model, run a query, or plan a migration.',
134
134
  );
135
135
  } else {
136
136
  steps.push(
@@ -140,7 +140,7 @@ export function buildNextSteps(options: {
140
140
  '3. Open prisma-next.md for a quick reference on how to write your first typed query.',
141
141
  );
142
142
  steps.push(
143
- '4. The .agents/skills/prisma-next/SKILL.md file is wired up for AI-coding agents in this project.',
143
+ '4. @prisma-next/agent-skill is registered with your agent runtime — open the project in your IDE and ask the agent to add a model, run a query, or plan a migration.',
144
144
  );
145
145
  }
146
146
  return steps;
@@ -253,7 +253,10 @@ export function dbFile(target: TargetId): string {
253
253
  import type { Contract } from './contract.d';
254
254
  import contractJson from './contract.json' with { type: 'json' };
255
255
 
256
- export const db = postgres<Contract>({ contractJson });
256
+ export const db = postgres<Contract>({
257
+ contractJson,
258
+ url: process.env['DATABASE_URL']!,
259
+ });
257
260
  `;
258
261
  }
259
262
 
@@ -1,138 +0,0 @@
1
- # Prisma Next — project skill
2
-
3
- This project uses **Prisma Next** with **MongoDB** via `@prisma-next/mongo`. Prisma Next lets the user define data models in a contract file and query them with a fully typed ORM.
4
-
5
- ## Files
6
-
7
- - **Contract**: `{{schemaPath}}` ({{authoringLabel}} authoring) — the user's data models. Edit this to add or change models.
8
- - **Config**: `prisma-next.config.ts` — tells the CLI where the contract is and how to connect to the database. Loads `.env` via `dotenv/config`.
9
- - **Database client**: `{{schemaDir}}/db.ts` — `import { db } from '{{dbImportPath}}'`. This is the entry point for all queries.
10
- - **Generated files** (do not edit by hand):
11
- - `{{schemaDir}}/contract.json` — compiled contract, used at runtime
12
- - `{{schemaDir}}/contract.d.ts` — TypeScript types for the contract, used for autocomplete and type checking
13
-
14
- ## Commands
15
-
16
- - `{{pkgRun}} contract emit` — regenerate `contract.json` and `contract.d.ts` after changing the contract
17
- - `{{pkgRun}} db init` — bootstrap a database to match the contract (creates collections, indexes, constraints). Additive only — won't drop existing structures.
18
- - `{{pkgRun}} db update` — update the database to match the current contract. Prompts for confirmation on destructive changes. Use `--dry-run` to preview.
19
- - `{{pkgRun}} migration plan` — create a new migration from contract changes (offline, no database needed). Use `--name <slug>` to name it.
20
- - `{{pkgRun}} migration apply` — apply pending migrations to the database
21
- - `{{pkgRun}} migration status` — show which migrations are applied and which are pending
22
- - `{{pkgRun}} migration show <name>` — show details of a specific migration
23
-
24
- ## How to write queries
25
-
26
- Use the ORM (`db.orm`). Each root accessor is the lowercased plural form emitted by `prisma-next contract emit` (typically the `@@map`-ped collection name) — for `model User { @@map("users") }` use `db.orm.users`, for `model Post { @@map("posts") }` use `db.orm.posts`. The Mongo facade has no raw-SQL surface. Two escape hatches exist for cases the ORM can't express; both are covered under "Escape hatches" below.
27
-
28
- ```typescript
29
- import { db } from '{{dbImportPath}}';
30
-
31
- // Find one record
32
- const user = await db.orm.users
33
- .where({ email: 'alice@example.com' })
34
- .first();
35
- // Returns { _id: ObjectId; email: string; ... } | null
36
-
37
- // Find multiple records — `.all()` returns an AsyncIterableResult, consume
38
- // either as an async iterable (below) or by `await`ing it to materialise an Array.
39
- for await (const user of db.orm.users
40
- .select('_id', 'email')
41
- .take(10)
42
- .all()) {
43
- // Each `user` is { _id: ObjectId; email: string }
44
- }
45
- // Returns AsyncIterableResult<{ _id: ObjectId; email: string }>
46
-
47
- // Filter, order, limit
48
- const recentPosts = await db.orm.posts
49
- .where({ authorId: userId })
50
- .orderBy({ createdAt: -1 })
51
- .select('_id', 'title', 'createdAt')
52
- .take(50)
53
- .all();
54
-
55
- // Include relations (reference relations only — embedded relations come back automatically)
56
- const usersWithPosts = await db.orm.users
57
- .select('_id', 'email')
58
- .include('posts')
59
- .take(10)
60
- .all();
61
- ```
62
-
63
- ### Key ORM methods
64
-
65
- - `.where({ field: value, ... })` — filter records by an equality object. Pass a raw filter expression for `$gt`/`$in`/`$regex` etc.
66
- - `.select('field1', 'field2', ...)` — pick which fields to return
67
- - `.orderBy({ field: 1 | -1 })` — sort results (1 = ascending, -1 = descending)
68
- - `.take(n)` / `.skip(n)` — limit and offset
69
- - `.all()` — execute and return all matching records as an `AsyncIterableResult`
70
- - `.first()` — execute with limit 1 and return the first matching row, or `null`
71
- - `.include('relationName')` — eager-load a reference relation (`$lookup`); embedded relations are already part of the row
72
- - `.variant('VariantName')` — narrow a polymorphic collection to a discriminator value
73
-
74
- ## Escape hatches
75
-
76
- The ORM covers the common cases. When you genuinely need something it can't express, prefer these — in order — over reaching for `db.runtime()` (which is an internal executor surface, not a `mongodb`-driver handle):
77
-
78
- 1. **Typed raw aggregations — `db.query`.** The facade exposes a `db.query` builder that runs aggregation pipelines through the same runtime + middleware + codec stack as `db.orm`, so results stay typed against the contract. Use this for `$lookup`/`$facet`/`$graphLookup`/window-function pipelines that the ORM doesn't surface.
79
-
80
- 2. **Direct `mongodb` driver control — `mongoClient` binding.** If you need a raw `MongoClient` (e.g. for transactions, change streams, sessions, or a driver feature Prisma Next doesn't expose), construct one yourself and pass it to `mongo({ mongoClient, dbName, contractJson })`. Your code keeps the `MongoClient` reference and uses it directly, while the same `db` object still gives you the typed ORM surface:
81
-
82
- ```typescript
83
- import { MongoClient } from 'mongodb';
84
- import mongo from '@prisma-next/mongo/runtime';
85
- import type { Contract } from './contract.d';
86
- import contractJson from './contract.json' with { type: 'json' };
87
-
88
- const client = new MongoClient(process.env['DATABASE_URL']!);
89
- await client.connect();
90
-
91
- export const db = mongo<Contract>({ contractJson, mongoClient: client, dbName: 'mydb' });
92
-
93
- const session = client.startSession();
94
- try {
95
- await session.withTransaction(async () => {
96
- await db.orm.users.createAll([{ /* ... */ }]);
97
- });
98
- } finally {
99
- await session.endSession();
100
- }
101
- ```
102
-
103
- ## Rules
104
-
105
- - **Never hand-edit** `contract.json` or `contract.d.ts`. Always regenerate them with `contract emit`.
106
- - **Always emit after contract changes.** When you modify `{{schemaPath}}`, run `{{pkgRun}} contract emit` before writing any code that depends on the new or changed models.
107
- - **Don't restructure `db.ts`.** It's scaffolded by init and works as-is. `db` connects lazily on the first query — there is no `db.connect(...)` step.
108
- - **Root accessors are emitter-driven.** Use the lowercased plural collection name (e.g. `db.orm.users`, `db.orm.posts`) — not the PascalCase model name. Re-run `{{pkgRun}} contract emit` if a new model's accessor isn't appearing on `db.orm`.
109
- - **Connection string** is `DATABASE_URL` in `.env`. If the user reports connection errors, check this value and the `.env` file.
110
- - **Transactions and change streams** require a MongoDB **replica set**. The Mongo facade does not yet expose `db.transaction(...)` — for now, use the `mongoClient` escape hatch above to drive transactions/sessions directly. See the quick reference for dev-environment options; the typed transaction API is tracked under [TML-2313](https://linear.app/prisma-company/issue/TML-2313/mongo-dev-replica-set-story-is-missing-transactions-change-streams).
111
- - **Don't reach for `db.runtime()`** as an escape hatch. It returns the internal executor (`MongoRuntime`), not a `mongodb` `MongoClient` or `Db`. Use `db.query` for raw aggregations and the `mongoClient` binding for direct driver control.
112
-
113
- ## Workflow for common tasks
114
-
115
- **User wants to add a new model or field:**
116
- 1. Edit `{{schemaPath}}`
117
- 2. Run `{{pkgRun}} contract emit`
118
- 3. Write query code using `db.orm.<collection>` (lowercased plural, see Rules above)
119
-
120
- **User wants to query data:**
121
- 1. Import `db` from `{{dbImportPath}}`
122
- 2. Use `db.orm.<collection>` with `.where()`, `.select()`, `.all()`, `.first()`, etc.
123
-
124
- **User wants to set up or change the database connection:**
125
- 1. Edit `DATABASE_URL` in `.env`
126
- 2. The config file (`prisma-next.config.ts`) reads it automatically via `dotenv/config`
127
-
128
- **User wants to set up the database for the first time:**
129
- 1. Run `{{pkgRun}} db init`
130
-
131
- **User wants to update the database after changing the contract:**
132
- 1. Quick path: `{{pkgRun}} db update` — compares the database to the contract and applies changes directly
133
- 2. Migration path (for production workflows):
134
- - `{{pkgRun}} migration plan --name describe-the-change` — creates a migration
135
- - `{{pkgRun}} migration apply` — applies pending migrations
136
-
137
- **User wants to check what migrations need to be applied:**
138
- 1. Run `{{pkgRun}} migration status`