@prisma-next/cli 0.5.0-dev.4 → 0.5.0-dev.6
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.
- package/dist/agent-skill-mongo.md +63 -31
- package/dist/agent-skill-postgres.md +1 -1
- package/dist/cli.mjs +119 -13
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-TG7rbCWT.mjs → client-CrsnY58k.mjs} +4 -4
- package/dist/{client-TG7rbCWT.mjs.map → client-CrsnY58k.mjs.map} +1 -1
- package/dist/commands/contract-emit.mjs +2 -2
- package/dist/commands/contract-infer.mjs +2 -2
- package/dist/commands/db-init.mjs +7 -7
- package/dist/commands/db-schema.mjs +5 -5
- package/dist/commands/db-sign.mjs +7 -7
- package/dist/commands/db-update.mjs +7 -7
- package/dist/commands/db-verify.mjs +7 -7
- package/dist/commands/migration-apply.mjs +6 -6
- package/dist/commands/migration-new.mjs +5 -5
- package/dist/commands/migration-plan.mjs +6 -6
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.mjs +4 -4
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.mjs +6 -6
- package/dist/commands/migration-status.mjs +2 -2
- package/dist/{config-loader-_W4T21X1.mjs → config-loader-C25b63rJ.mjs} +1 -1
- package/dist/{config-loader-_W4T21X1.mjs.map → config-loader-C25b63rJ.mjs.map} +1 -1
- package/dist/config-loader.mjs +1 -1
- package/dist/contract-emit--feXyNd7.mjs +4 -0
- package/dist/{contract-emit-DpPjuFy-.mjs → contract-emit-NJ01hiiv.mjs} +8 -8
- package/dist/{contract-emit-DpPjuFy-.mjs.map → contract-emit-NJ01hiiv.mjs.map} +1 -1
- package/dist/{contract-emit-CQfj7xJn.mjs → contract-emit-V5SSitUT.mjs} +6 -6
- package/dist/{contract-emit-CQfj7xJn.mjs.map → contract-emit-V5SSitUT.mjs.map} +1 -1
- package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-CAOELa-H.mjs} +1 -1
- package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-CAOELa-H.mjs.map} +1 -1
- package/dist/{contract-infer-BS4kIX9c.mjs → contract-infer-D9cC3rJm.mjs} +4 -4
- package/dist/{contract-infer-BS4kIX9c.mjs.map → contract-infer-D9cC3rJm.mjs.map} +1 -1
- package/dist/exports/control-api.mjs +4 -4
- package/dist/exports/index.mjs +2 -2
- package/dist/exports/init-output.d.mts +39 -0
- package/dist/exports/init-output.d.mts.map +1 -0
- package/dist/exports/init-output.mjs +3 -0
- package/dist/{extract-operation-statements-DZUJNmL3.mjs → extract-operation-statements-DsFfxXVZ.mjs} +2 -2
- package/dist/{extract-operation-statements-DZUJNmL3.mjs.map → extract-operation-statements-DsFfxXVZ.mjs.map} +1 -1
- package/dist/{extract-sql-ddl-DDMX-9mz.mjs → extract-sql-ddl-D9UbZDyz.mjs} +1 -1
- package/dist/{extract-sql-ddl-DDMX-9mz.mjs.map → extract-sql-ddl-D9UbZDyz.mjs.map} +1 -1
- package/dist/{framework-components-DfZKQBQ2.mjs → framework-components-Cr--XBKy.mjs} +2 -2
- package/dist/{framework-components-DfZKQBQ2.mjs.map → framework-components-Cr--XBKy.mjs.map} +1 -1
- package/dist/init-C5220SY9.mjs +2062 -0
- package/dist/init-C5220SY9.mjs.map +1 -0
- package/dist/{inspect-live-schema-BsoFVoS1.mjs → inspect-live-schema-yrHAvG71.mjs} +6 -6
- package/dist/{inspect-live-schema-BsoFVoS1.mjs.map → inspect-live-schema-yrHAvG71.mjs.map} +1 -1
- package/dist/migration-cli.mjs +1 -1
- package/dist/{migration-command-scaffold-DOXnheFa.mjs → migration-command-scaffold-B3B09et6.mjs} +6 -6
- package/dist/{migration-command-scaffold-DOXnheFa.mjs.map → migration-command-scaffold-B3B09et6.mjs.map} +1 -1
- package/dist/{migration-status-Ry3TnEya.mjs → migration-status-DUMiH8_G.mjs} +6 -6
- package/dist/{migration-status-Ry3TnEya.mjs.map → migration-status-DUMiH8_G.mjs.map} +1 -1
- package/dist/{migrations-fU0xoKjS.mjs → migrations-Bo5WtTla.mjs} +2 -2
- package/dist/{migrations-fU0xoKjS.mjs.map → migrations-Bo5WtTla.mjs.map} +1 -1
- package/dist/output-BpcQrnnq.mjs +103 -0
- package/dist/output-BpcQrnnq.mjs.map +1 -0
- package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-DvQWB1nK.mjs} +1 -1
- package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DvQWB1nK.mjs.map} +1 -1
- package/dist/quick-reference-mongo.md +34 -13
- package/dist/quick-reference-postgres.md +11 -9
- package/dist/{result-handler-BJwA7ufw.mjs → result-handler-Ba3zWQsI.mjs} +4 -77
- package/dist/result-handler-Ba3zWQsI.mjs.map +1 -0
- package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-C3ZLwQxK.mjs} +76 -2
- package/dist/terminal-ui-C3ZLwQxK.mjs.map +1 -0
- package/dist/{validate-contract-deps-esa-VQ0h.mjs → validate-contract-deps-B_Cs29TL.mjs} +1 -1
- package/dist/{validate-contract-deps-esa-VQ0h.mjs.map → validate-contract-deps-B_Cs29TL.mjs.map} +1 -1
- package/dist/{verify-bl__PkXk.mjs → verify-Bkycc-Tf.mjs} +2 -2
- package/dist/{verify-bl__PkXk.mjs.map → verify-Bkycc-Tf.mjs.map} +1 -1
- package/package.json +21 -16
- package/src/commands/init/detect-pnpm-catalog.ts +141 -0
- package/src/commands/init/errors.ts +254 -0
- package/src/commands/init/exit-codes.ts +62 -0
- package/src/commands/init/hygiene-gitattributes.ts +97 -0
- package/src/commands/init/hygiene-gitignore.ts +48 -0
- package/src/commands/init/hygiene-package-scripts.ts +91 -0
- package/src/commands/init/index.ts +112 -7
- package/src/commands/init/init.ts +766 -144
- package/src/commands/init/inputs.ts +421 -0
- package/src/commands/init/output.ts +147 -0
- package/src/commands/init/probe-db.ts +308 -0
- package/src/commands/init/reinit-cleanup.ts +83 -0
- package/src/commands/init/templates/agent-skill-mongo.md +63 -31
- package/src/commands/init/templates/agent-skill-postgres.md +1 -1
- package/src/commands/init/templates/agent-skill.ts +25 -3
- package/src/commands/init/templates/code-templates.ts +125 -32
- package/src/commands/init/templates/env.ts +80 -0
- package/src/commands/init/templates/quick-reference-mongo.md +34 -13
- package/src/commands/init/templates/quick-reference-postgres.md +11 -9
- package/src/commands/init/templates/quick-reference.ts +42 -3
- package/src/commands/init/templates/tsconfig.ts +167 -5
- package/src/exports/init-output.ts +10 -0
- package/dist/contract-emit-fhNwwhkQ.mjs +0 -4
- package/dist/init-CQfo_4Ro.mjs +0 -430
- package/dist/init-CQfo_4Ro.mjs.map +0 -1
- package/dist/result-handler-BJwA7ufw.mjs.map +0 -1
- package/dist/terminal-ui-C5k88MmW.mjs.map +0 -1
- /package/dist/{cli-errors-C0JhVj0c.d.mts → cli-errors-BFYgBH3L.d.mts} +0 -0
- /package/dist/{cli-errors-DHq6GQGu.mjs → cli-errors-Cd79vmTH.mjs} +0 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { join } from 'pathe';
|
|
3
|
+
import type { TargetId } from './templates/code-templates';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Result of an attempted database probe (FR8.3). `kind` is the
|
|
7
|
+
* machine-readable status; `message` is the human-readable line we
|
|
8
|
+
* surface in `init`'s warning channel (or, under `--strict-probe`, in
|
|
9
|
+
* the structured error). The probe never throws — every failure
|
|
10
|
+
* mode is folded into one of these variants so `runInit` can branch
|
|
11
|
+
* exactly once.
|
|
12
|
+
*/
|
|
13
|
+
export type ProbeOutcome =
|
|
14
|
+
| {
|
|
15
|
+
readonly kind: 'ok';
|
|
16
|
+
readonly serverVersion: string;
|
|
17
|
+
readonly minVersion: string;
|
|
18
|
+
readonly meetsMinimum: true;
|
|
19
|
+
readonly message: string;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
readonly kind: 'below-minimum';
|
|
23
|
+
readonly serverVersion: string;
|
|
24
|
+
readonly minVersion: string;
|
|
25
|
+
readonly meetsMinimum: false;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
readonly kind: 'no-database-url';
|
|
30
|
+
readonly minVersion: string;
|
|
31
|
+
readonly meetsMinimum: null;
|
|
32
|
+
readonly message: string;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
readonly kind: 'connection-failed';
|
|
36
|
+
readonly minVersion: string;
|
|
37
|
+
readonly meetsMinimum: null;
|
|
38
|
+
readonly cause: string;
|
|
39
|
+
readonly message: string;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
readonly kind: 'driver-missing';
|
|
43
|
+
readonly minVersion: string;
|
|
44
|
+
readonly meetsMinimum: null;
|
|
45
|
+
readonly cause: string;
|
|
46
|
+
readonly message: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export interface ProbeContext {
|
|
50
|
+
readonly baseDir: string;
|
|
51
|
+
readonly target: TargetId;
|
|
52
|
+
readonly databaseUrl: string | undefined;
|
|
53
|
+
readonly minVersion: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Optional injection seam exposed for unit tests so the probe logic
|
|
58
|
+
* (env handling, version parsing, comparator, message formatting) can
|
|
59
|
+
* be exercised without a live database. Production callers omit this
|
|
60
|
+
* argument and get the real `pg` / `mongodb` driver path.
|
|
61
|
+
*/
|
|
62
|
+
export interface ProbeOverrides {
|
|
63
|
+
readonly probePostgres?: (databaseUrl: string) => Promise<DriverResult>;
|
|
64
|
+
readonly probeMongo?: (databaseUrl: string) => Promise<DriverResult>;
|
|
65
|
+
readonly requireFromBaseDir?: (baseDir: string, moduleId: string) => unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface DriverResult {
|
|
69
|
+
readonly serverVersion: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Connects (when configured) to the user's database and returns a
|
|
74
|
+
* structured outcome describing whether the server meets the declared
|
|
75
|
+
* minimum (FR8.1). Pure with respect to its inputs: no I/O happens
|
|
76
|
+
* unless `databaseUrl` is set.
|
|
77
|
+
*
|
|
78
|
+
* The outcome is shaped so that `--strict-probe` can branch on the
|
|
79
|
+
* `kind`/`meetsMinimum` pair without re-stringifying the message:
|
|
80
|
+
*
|
|
81
|
+
* - `ok` — informational; `init` continues.
|
|
82
|
+
* - `below-minimum` — warning; `init` continues regardless of
|
|
83
|
+
* `--strict-probe` (the spec scopes strict-probe to "probe
|
|
84
|
+
* *failures*", and a successful probe that finds an old server is
|
|
85
|
+
* not a failure).
|
|
86
|
+
* - `no-database-url` / `connection-failed` / `driver-missing` —
|
|
87
|
+
* warning by default, fatal under `--strict-probe`.
|
|
88
|
+
*/
|
|
89
|
+
export async function probeServerVersion(
|
|
90
|
+
ctx: ProbeContext,
|
|
91
|
+
overrides: ProbeOverrides = {},
|
|
92
|
+
): Promise<ProbeOutcome> {
|
|
93
|
+
const { databaseUrl, minVersion, target } = ctx;
|
|
94
|
+
if (databaseUrl === undefined || databaseUrl.trim().length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
kind: 'no-database-url',
|
|
97
|
+
minVersion,
|
|
98
|
+
meetsMinimum: null,
|
|
99
|
+
message:
|
|
100
|
+
'Skipped --probe-db: DATABASE_URL is not set in the current shell environment. (init does not read .env for the probe; export the variable or drop --probe-db.)',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let driverResult: DriverResult;
|
|
105
|
+
try {
|
|
106
|
+
if (target === 'postgres') {
|
|
107
|
+
driverResult =
|
|
108
|
+
overrides.probePostgres !== undefined
|
|
109
|
+
? await overrides.probePostgres(databaseUrl)
|
|
110
|
+
: await defaultProbePostgres(databaseUrl, ctx.baseDir, overrides);
|
|
111
|
+
} else {
|
|
112
|
+
driverResult =
|
|
113
|
+
overrides.probeMongo !== undefined
|
|
114
|
+
? await overrides.probeMongo(databaseUrl)
|
|
115
|
+
: await defaultProbeMongo(databaseUrl, ctx.baseDir, overrides);
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err instanceof DriverMissingError) {
|
|
119
|
+
return {
|
|
120
|
+
kind: 'driver-missing',
|
|
121
|
+
minVersion,
|
|
122
|
+
meetsMinimum: null,
|
|
123
|
+
cause: err.message,
|
|
124
|
+
message: `Skipped --probe-db: ${err.message}. (Run with install enabled, or install the driver yourself, then re-run \`prisma-next init --probe-db\`.)`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const cause = redactDatabaseUrlSecrets(causeMessage(err));
|
|
128
|
+
return {
|
|
129
|
+
kind: 'connection-failed',
|
|
130
|
+
minVersion,
|
|
131
|
+
meetsMinimum: null,
|
|
132
|
+
cause,
|
|
133
|
+
message: `--probe-db could not connect: ${cause}.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const meets = compareVersionPrefix(driverResult.serverVersion, minVersion);
|
|
138
|
+
if (meets < 0) {
|
|
139
|
+
return {
|
|
140
|
+
kind: 'below-minimum',
|
|
141
|
+
serverVersion: driverResult.serverVersion,
|
|
142
|
+
minVersion,
|
|
143
|
+
meetsMinimum: false,
|
|
144
|
+
message: `--probe-db: server reports version ${driverResult.serverVersion}, below the declared minimum (${minVersion}). Some queries may fail until the server is upgraded.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
kind: 'ok',
|
|
149
|
+
serverVersion: driverResult.serverVersion,
|
|
150
|
+
minVersion,
|
|
151
|
+
meetsMinimum: true,
|
|
152
|
+
message: `--probe-db: server reports version ${driverResult.serverVersion} (>= ${minVersion}).`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Compares two semver-prefix strings ("14", "14.2", "6.0", …) by
|
|
158
|
+
* numeric components left-to-right. Returns a negative number when `a`
|
|
159
|
+
* is older than `b`, zero when both versions agree on every numeric
|
|
160
|
+
* component (treating missing trailing components as `0`), and a
|
|
161
|
+
* positive number when `a` is newer.
|
|
162
|
+
*
|
|
163
|
+
* The loop runs over the **longer** of the two prefixes so that
|
|
164
|
+
* `'14'` compares less than `'14.1'` — without that, the shorter
|
|
165
|
+
* prefix would be silently accepted whenever the configured minimum
|
|
166
|
+
* has a non-zero minor or patch.
|
|
167
|
+
*
|
|
168
|
+
* Exported for unit tests.
|
|
169
|
+
*/
|
|
170
|
+
export function compareVersionPrefix(a: string, b: string): number {
|
|
171
|
+
const aParts = parseNumericParts(a);
|
|
172
|
+
const bParts = parseNumericParts(b);
|
|
173
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
174
|
+
for (let i = 0; i < len; i += 1) {
|
|
175
|
+
const aPart = aParts[i] ?? 0;
|
|
176
|
+
const bPart = bParts[i] ?? 0;
|
|
177
|
+
if (aPart !== bPart) return aPart - bPart;
|
|
178
|
+
}
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseNumericParts(version: string): readonly number[] {
|
|
183
|
+
const match = version.match(/^[^\d]*(\d+(?:\.\d+){0,3})/);
|
|
184
|
+
if (match === null) return [];
|
|
185
|
+
return (match[1] ?? '').split('.').map((part) => Number.parseInt(part, 10));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
class DriverMissingError extends Error {}
|
|
189
|
+
|
|
190
|
+
function causeMessage(err: unknown): string {
|
|
191
|
+
if (err instanceof Error) return err.message;
|
|
192
|
+
return String(err);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Strips `user:password@` userinfo from any URL-shaped substring before
|
|
197
|
+
* we surface the cause to the user. Mirrors `redactSecrets` in
|
|
198
|
+
* `init.ts` — the probe path has its own redactor because the inputs
|
|
199
|
+
* here include the raw connection string by construction (driver
|
|
200
|
+
* errors echo the URL back).
|
|
201
|
+
*
|
|
202
|
+
* Exported for unit tests.
|
|
203
|
+
*/
|
|
204
|
+
export function redactDatabaseUrlSecrets(text: string): string {
|
|
205
|
+
if (!text) return text;
|
|
206
|
+
return text.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^/@\s]+)@/g, '$1***@');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function defaultProbePostgres(
|
|
210
|
+
databaseUrl: string,
|
|
211
|
+
baseDir: string,
|
|
212
|
+
overrides: ProbeOverrides,
|
|
213
|
+
): Promise<DriverResult> {
|
|
214
|
+
const pg = requirePeer<{ Client: new (cfg: { connectionString: string }) => PgClient }>(
|
|
215
|
+
'pg',
|
|
216
|
+
baseDir,
|
|
217
|
+
overrides,
|
|
218
|
+
);
|
|
219
|
+
const client = new pg.Client({ connectionString: databaseUrl });
|
|
220
|
+
await client.connect();
|
|
221
|
+
try {
|
|
222
|
+
const result = await client.query('SELECT version() as version');
|
|
223
|
+
const versionString = String(result?.rows?.[0]?.version ?? '');
|
|
224
|
+
const parsed = parsePostgresVersion(versionString);
|
|
225
|
+
return { serverVersion: parsed };
|
|
226
|
+
} finally {
|
|
227
|
+
await client.end().catch(() => undefined);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
interface PgClient {
|
|
232
|
+
connect(): Promise<void>;
|
|
233
|
+
query(sql: string): Promise<{ rows: ReadonlyArray<{ version: string }> }>;
|
|
234
|
+
end(): Promise<void>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Extracts the numeric prefix from a Postgres `version()` row, e.g.
|
|
239
|
+
*
|
|
240
|
+
* `PostgreSQL 14.10 on x86_64-pc-linux-gnu, ...` → `"14.10"`
|
|
241
|
+
* `PostgreSQL 16beta1 on …` → `"16"` (we
|
|
242
|
+
* conservatively drop the suffix; minimum-version comparisons
|
|
243
|
+
* treat 16beta1 as 16, which is what every reasonable user
|
|
244
|
+
* expects).
|
|
245
|
+
*
|
|
246
|
+
* Exported for unit tests.
|
|
247
|
+
*/
|
|
248
|
+
export function parsePostgresVersion(versionString: string): string {
|
|
249
|
+
const match = versionString.match(/PostgreSQL\s+(\d+(?:\.\d+)?)/i);
|
|
250
|
+
if (match === null || match[1] === undefined) {
|
|
251
|
+
throw new Error(`Could not parse PostgreSQL version from \`${versionString}\``);
|
|
252
|
+
}
|
|
253
|
+
return match[1];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function defaultProbeMongo(
|
|
257
|
+
databaseUrl: string,
|
|
258
|
+
baseDir: string,
|
|
259
|
+
overrides: ProbeOverrides,
|
|
260
|
+
): Promise<DriverResult> {
|
|
261
|
+
const mongodb = requirePeer<{
|
|
262
|
+
MongoClient: new (
|
|
263
|
+
url: string,
|
|
264
|
+
) => {
|
|
265
|
+
connect(): Promise<unknown>;
|
|
266
|
+
db(name?: string): {
|
|
267
|
+
admin(): { command(cmd: Record<string, unknown>): Promise<{ version?: string }> };
|
|
268
|
+
};
|
|
269
|
+
close(): Promise<void>;
|
|
270
|
+
};
|
|
271
|
+
}>('mongodb', baseDir, overrides);
|
|
272
|
+
const client = new mongodb.MongoClient(databaseUrl);
|
|
273
|
+
await client.connect();
|
|
274
|
+
try {
|
|
275
|
+
const buildInfo = await client.db().admin().command({ buildInfo: 1 });
|
|
276
|
+
const versionString = String(buildInfo.version ?? '');
|
|
277
|
+
if (versionString.length === 0) {
|
|
278
|
+
throw new Error('buildInfo did not include a `version` field');
|
|
279
|
+
}
|
|
280
|
+
return { serverVersion: versionString };
|
|
281
|
+
} finally {
|
|
282
|
+
await client.close().catch(() => undefined);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Loads a peer driver (`pg` / `mongodb`) from the user's project
|
|
288
|
+
* `node_modules`. We deliberately resolve from `baseDir` rather than
|
|
289
|
+
* from the CLI bundle — the CLI does not depend on `pg` or `mongodb`
|
|
290
|
+
* directly, but the user's `init`-generated `package.json` does (via
|
|
291
|
+
* the target facade). Failure to resolve is folded into a typed
|
|
292
|
+
* `DriverMissingError` so `probeServerVersion` can map it to a
|
|
293
|
+
* `driver-missing` outcome rather than letting a `MODULE_NOT_FOUND`
|
|
294
|
+
* leak as a generic connection failure.
|
|
295
|
+
*/
|
|
296
|
+
function requirePeer<T>(moduleId: string, baseDir: string, overrides: ProbeOverrides): T {
|
|
297
|
+
try {
|
|
298
|
+
if (overrides.requireFromBaseDir !== undefined) {
|
|
299
|
+
return overrides.requireFromBaseDir(baseDir, moduleId) as T;
|
|
300
|
+
}
|
|
301
|
+
const requireFromBase = createRequire(join(baseDir, 'package.json'));
|
|
302
|
+
return requireFromBase(moduleId) as T;
|
|
303
|
+
} catch (err) {
|
|
304
|
+
throw new DriverMissingError(
|
|
305
|
+
`\`${moduleId}\` is not installed in this project (resolved from ${baseDir}; cause: ${causeMessage(err)})`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'pathe';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Filenames the contract pipeline emits next to the user's schema source
|
|
6
|
+
* (`<schemaDir>/contract.json`, `<schemaDir>/contract.d.ts`, …). Mirrors
|
|
7
|
+
* `ARTEFACT_FILENAMES` in `hygiene-gitattributes.ts`; kept as a separate
|
|
8
|
+
* constant here because the cleanup contract is target-agnostic and we
|
|
9
|
+
* deliberately do not want a stale `start-contract.json` from a previous
|
|
10
|
+
* target lingering after a re-init.
|
|
11
|
+
*
|
|
12
|
+
* If a future emit pipeline produces an additional artefact, add it here
|
|
13
|
+
* **and** to the gitattributes list — the two stay in lockstep so the
|
|
14
|
+
* file `init` advertises as `linguist-generated` is exactly the file
|
|
15
|
+
* `init` is willing to delete on re-init.
|
|
16
|
+
*/
|
|
17
|
+
const ARTEFACT_FILENAMES: readonly string[] = [
|
|
18
|
+
'contract.json',
|
|
19
|
+
'contract.d.ts',
|
|
20
|
+
'end-contract.json',
|
|
21
|
+
'end-contract.d.ts',
|
|
22
|
+
'start-contract.json',
|
|
23
|
+
'start-contract.d.ts',
|
|
24
|
+
'ops.json',
|
|
25
|
+
'migration.json',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns the schema-relative paths of stale contract artefacts the
|
|
30
|
+
* previous `init` run (or a `contract emit`) left behind in `schemaDir`.
|
|
31
|
+
* Paths are returned relative to `baseDir` so the caller can plumb them
|
|
32
|
+
* into `filesWritten`-style logging without re-deriving the path.
|
|
33
|
+
*
|
|
34
|
+
* Pure function: no filesystem mutation. Used by `runInit`'s precondition
|
|
35
|
+
* phase (FR6.2 / NFR3 atomicity) so a downstream parse failure leaves
|
|
36
|
+
* the artefacts on disk and the project byte-identical to its pre-init
|
|
37
|
+
* state.
|
|
38
|
+
*/
|
|
39
|
+
export function findStaleArtefacts(baseDir: string, schemaDir: string): readonly string[] {
|
|
40
|
+
const result: string[] = [];
|
|
41
|
+
for (const filename of ARTEFACT_FILENAMES) {
|
|
42
|
+
const rel = join(schemaDir, filename);
|
|
43
|
+
if (existsSync(join(baseDir, rel))) {
|
|
44
|
+
result.push(rel);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Drops a single key from `package.json#dependencies`, returning the new
|
|
52
|
+
* file content. Returns `null` when the dependency was already absent —
|
|
53
|
+
* the caller can skip the write to keep re-init idempotent (FR9.3).
|
|
54
|
+
*
|
|
55
|
+
* Used by `runInit` for the FR9.2 target-switch path: when the user
|
|
56
|
+
* re-inits a project from `--target postgres` to `--target mongodb` (or
|
|
57
|
+
* vice versa), the previous facade is removed from `dependencies` so the
|
|
58
|
+
* resulting project depends only on the chosen target's facade.
|
|
59
|
+
*
|
|
60
|
+
* Devs/peers/optional dep groups are intentionally *not* touched — the
|
|
61
|
+
* facades are only ever in `dependencies` (FR4 / FR7), and broadening
|
|
62
|
+
* the search would risk clobbering an unrelated dep with the same name
|
|
63
|
+
* in `peerDependencies`.
|
|
64
|
+
*
|
|
65
|
+
* Throws `SyntaxError` if `existing` is not parseable as JSON; the
|
|
66
|
+
* caller (`runInit`) already guards on that with a structured 5010
|
|
67
|
+
* error before this helper is reached.
|
|
68
|
+
*/
|
|
69
|
+
export function removeDependency(existing: string, depName: string): string | null {
|
|
70
|
+
const parsed = JSON.parse(existing) as Record<string, unknown>;
|
|
71
|
+
const deps = parsed['dependencies'];
|
|
72
|
+
if (deps === null || typeof deps !== 'object' || Array.isArray(deps)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (!Object.hasOwn(deps as Record<string, unknown>, depName)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const next = { ...(deps as Record<string, unknown>) };
|
|
79
|
+
delete next[depName];
|
|
80
|
+
parsed['dependencies'] = next;
|
|
81
|
+
const trailingNewline = existing.endsWith('\n') ? '\n' : '';
|
|
82
|
+
return `${JSON.stringify(parsed, null, 2)}${trailingNewline}`;
|
|
83
|
+
}
|
|
@@ -4,7 +4,7 @@ This project uses **Prisma Next** with **MongoDB** via `@prisma-next/mongo`. Pri
|
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
7
|
-
- **Contract**: `{{schemaPath}}` — the user's data models. Edit this to add or change models.
|
|
7
|
+
- **Contract**: `{{schemaPath}}` ({{authoringLabel}} authoring) — the user's data models. Edit this to add or change models.
|
|
8
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
9
|
- **Database client**: `{{schemaDir}}/db.ts` — `import { db } from '{{dbImportPath}}'`. This is the entry point for all queries.
|
|
10
10
|
- **Generated files** (do not edit by hand):
|
|
@@ -23,71 +23,103 @@ This project uses **Prisma Next** with **MongoDB** via `@prisma-next/mongo`. Pri
|
|
|
23
23
|
|
|
24
24
|
## How to write queries
|
|
25
25
|
|
|
26
|
-
|
|
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
27
|
|
|
28
28
|
```typescript
|
|
29
29
|
import { db } from '{{dbImportPath}}';
|
|
30
30
|
|
|
31
31
|
// Find one record
|
|
32
|
-
const user = await db.orm.
|
|
33
|
-
.where(
|
|
32
|
+
const user = await db.orm.users
|
|
33
|
+
.where({ email: 'alice@example.com' })
|
|
34
34
|
.first();
|
|
35
|
-
// Returns {
|
|
35
|
+
// Returns { _id: ObjectId; email: string; ... } | null
|
|
36
36
|
|
|
37
|
-
// Find multiple records
|
|
38
|
-
|
|
39
|
-
|
|
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')
|
|
40
41
|
.take(10)
|
|
41
|
-
.all()
|
|
42
|
-
//
|
|
42
|
+
.all()) {
|
|
43
|
+
// Each `user` is { _id: ObjectId; email: string }
|
|
44
|
+
}
|
|
45
|
+
// Returns AsyncIterableResult<{ _id: ObjectId; email: string }>
|
|
43
46
|
|
|
44
47
|
// Filter, order, limit
|
|
45
|
-
const recentPosts = await db.orm.
|
|
46
|
-
.where(
|
|
47
|
-
.orderBy(
|
|
48
|
-
.select('
|
|
48
|
+
const recentPosts = await db.orm.posts
|
|
49
|
+
.where({ authorId: userId })
|
|
50
|
+
.orderBy({ createdAt: -1 })
|
|
51
|
+
.select('_id', 'title', 'createdAt')
|
|
49
52
|
.take(50)
|
|
50
53
|
.all();
|
|
51
54
|
|
|
52
|
-
// Include relations
|
|
53
|
-
const usersWithPosts = await db.orm.
|
|
54
|
-
.select('
|
|
55
|
-
.include('posts'
|
|
56
|
-
post.select('id', 'title').orderBy(p => p.createdAt.desc()).take(5)
|
|
57
|
-
)
|
|
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')
|
|
58
59
|
.take(10)
|
|
59
60
|
.all();
|
|
60
61
|
```
|
|
61
62
|
|
|
62
63
|
### Key ORM methods
|
|
63
64
|
|
|
64
|
-
- `.where(
|
|
65
|
+
- `.where({ field: value, ... })` — filter records by an equality object. Pass a raw filter expression for `$gt`/`$in`/`$regex` etc.
|
|
65
66
|
- `.select('field1', 'field2', ...)` — pick which fields to return
|
|
66
|
-
- `.orderBy(
|
|
67
|
-
- `.take(n)` — limit
|
|
68
|
-
- `.all()` — execute and return all matching records as an
|
|
69
|
-
- `.first()` — execute and return the first matching
|
|
70
|
-
- `.
|
|
71
|
-
- `.
|
|
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
|
+
```
|
|
72
102
|
|
|
73
103
|
## Rules
|
|
74
104
|
|
|
75
105
|
- **Never hand-edit** `contract.json` or `contract.d.ts`. Always regenerate them with `contract emit`.
|
|
76
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.
|
|
77
|
-
- **Don't restructure `db.ts`.** It's scaffolded by init and works as-is.
|
|
78
|
-
- **Use `db.orm`
|
|
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`.
|
|
79
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.
|
|
80
112
|
|
|
81
113
|
## Workflow for common tasks
|
|
82
114
|
|
|
83
115
|
**User wants to add a new model or field:**
|
|
84
116
|
1. Edit `{{schemaPath}}`
|
|
85
117
|
2. Run `{{pkgRun}} contract emit`
|
|
86
|
-
3. Write query code using `db.orm
|
|
118
|
+
3. Write query code using `db.orm.<collection>` (lowercased plural, see Rules above)
|
|
87
119
|
|
|
88
120
|
**User wants to query data:**
|
|
89
121
|
1. Import `db` from `{{dbImportPath}}`
|
|
90
|
-
2. Use `db.orm
|
|
122
|
+
2. Use `db.orm.<collection>` with `.where()`, `.select()`, `.all()`, `.first()`, etc.
|
|
91
123
|
|
|
92
124
|
**User wants to set up or change the database connection:**
|
|
93
125
|
1. Edit `DATABASE_URL` in `.env`
|
|
@@ -4,7 +4,7 @@ This project uses **Prisma Next** with **PostgreSQL** via `@prisma-next/postgres
|
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
7
|
-
- **Contract**: `{{schemaPath}}` — the user's data models. Edit this to add or change models.
|
|
7
|
+
- **Contract**: `{{schemaPath}}` ({{authoringLabel}} authoring) — the user's data models. Edit this to add or change models.
|
|
8
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
9
|
- **Database client**: `{{schemaDir}}/db.ts` — `import { db } from '{{dbImportPath}}'`. This is the entry point for all queries.
|
|
10
10
|
- **Generated files** (do not edit by hand):
|
|
@@ -1,18 +1,40 @@
|
|
|
1
1
|
import { dirname } from 'pathe';
|
|
2
|
-
import type { TargetId } from './code-templates';
|
|
2
|
+
import type { AuthoringId, TargetId } from './code-templates';
|
|
3
3
|
import { renderTemplate } from './render';
|
|
4
4
|
|
|
5
|
-
export const variables = [
|
|
5
|
+
export const variables = [
|
|
6
|
+
'schemaPath',
|
|
7
|
+
'schemaDir',
|
|
8
|
+
'dbImportPath',
|
|
9
|
+
'pkgRun',
|
|
10
|
+
'authoringLabel',
|
|
11
|
+
] as const;
|
|
6
12
|
|
|
7
13
|
type TemplateVars = Record<(typeof variables)[number], string>;
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Renders the per-project agent skill (FR5.2). The skill template is
|
|
17
|
+
* target-specific (Postgres vs Mongo query syntax differs); the authoring
|
|
18
|
+
* style enters via:
|
|
19
|
+
*
|
|
20
|
+
* - `schemaPath` — already routed through {@link agentSkillMd}'s caller
|
|
21
|
+
* (the AC says a TS-authoring scaffold must reference `prisma/contract.ts`).
|
|
22
|
+
* - `authoringLabel` — a short human-readable note (`PSL` / `TypeScript`)
|
|
23
|
+
* the skill template uses when describing the contract file.
|
|
24
|
+
*/
|
|
25
|
+
export function agentSkillMd(
|
|
26
|
+
target: TargetId,
|
|
27
|
+
authoring: AuthoringId,
|
|
28
|
+
schemaPath: string,
|
|
29
|
+
pkgRun: string,
|
|
30
|
+
): string {
|
|
10
31
|
const schemaDir = dirname(schemaPath);
|
|
11
32
|
const vars: TemplateVars = {
|
|
12
33
|
schemaPath,
|
|
13
34
|
schemaDir,
|
|
14
35
|
dbImportPath: `./${schemaDir}/db`,
|
|
15
36
|
pkgRun,
|
|
37
|
+
authoringLabel: authoring === 'typescript' ? 'TypeScript' : 'PSL',
|
|
16
38
|
};
|
|
17
39
|
const templateFile = `agent-skill-${target}.md`;
|
|
18
40
|
return renderTemplate(templateFile, variables, vars);
|