@prisma-next/cli 0.5.0-dev.3 → 0.5.0-dev.5

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.
Files changed (107) hide show
  1. package/dist/agent-skill-mongo.md +63 -31
  2. package/dist/agent-skill-postgres.md +1 -1
  3. package/dist/cli.mjs +119 -13
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/{client-TG7rbCWT.mjs → client-CrsnY58k.mjs} +4 -4
  6. package/dist/{client-TG7rbCWT.mjs.map → client-CrsnY58k.mjs.map} +1 -1
  7. package/dist/commands/contract-emit.mjs +2 -2
  8. package/dist/commands/contract-infer.mjs +2 -2
  9. package/dist/commands/db-init.mjs +7 -7
  10. package/dist/commands/db-schema.mjs +5 -5
  11. package/dist/commands/db-sign.mjs +7 -7
  12. package/dist/commands/db-update.mjs +7 -7
  13. package/dist/commands/db-verify.mjs +7 -7
  14. package/dist/commands/migration-apply.mjs +8 -8
  15. package/dist/commands/migration-apply.mjs.map +1 -1
  16. package/dist/commands/migration-new.mjs +5 -5
  17. package/dist/commands/migration-plan.mjs +6 -6
  18. package/dist/commands/migration-ref.d.mts +6 -4
  19. package/dist/commands/migration-ref.d.mts.map +1 -1
  20. package/dist/commands/migration-ref.mjs +29 -34
  21. package/dist/commands/migration-ref.mjs.map +1 -1
  22. package/dist/commands/migration-show.d.mts +1 -1
  23. package/dist/commands/migration-show.mjs +6 -6
  24. package/dist/commands/migration-status.d.mts.map +1 -1
  25. package/dist/commands/migration-status.mjs +2 -2
  26. package/dist/{config-loader-_W4T21X1.mjs → config-loader-C25b63rJ.mjs} +1 -1
  27. package/dist/{config-loader-_W4T21X1.mjs.map → config-loader-C25b63rJ.mjs.map} +1 -1
  28. package/dist/config-loader.mjs +1 -1
  29. package/dist/contract-emit--feXyNd7.mjs +4 -0
  30. package/dist/{contract-emit-CNYyzJwF.mjs → contract-emit-NJ01hiiv.mjs} +8 -8
  31. package/dist/{contract-emit-CNYyzJwF.mjs.map → contract-emit-NJ01hiiv.mjs.map} +1 -1
  32. package/dist/{contract-emit-CQfj7xJn.mjs → contract-emit-V5SSitUT.mjs} +6 -6
  33. package/dist/{contract-emit-CQfj7xJn.mjs.map → contract-emit-V5SSitUT.mjs.map} +1 -1
  34. package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-CAOELa-H.mjs} +1 -1
  35. package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-CAOELa-H.mjs.map} +1 -1
  36. package/dist/{contract-infer-BP3DrGgz.mjs → contract-infer-D9cC3rJm.mjs} +4 -4
  37. package/dist/{contract-infer-BP3DrGgz.mjs.map → contract-infer-D9cC3rJm.mjs.map} +1 -1
  38. package/dist/exports/control-api.mjs +4 -4
  39. package/dist/exports/index.mjs +2 -2
  40. package/dist/exports/init-output.d.mts +39 -0
  41. package/dist/exports/init-output.d.mts.map +1 -0
  42. package/dist/exports/init-output.mjs +3 -0
  43. package/dist/{extract-operation-statements-DZUJNmL3.mjs → extract-operation-statements-DsFfxXVZ.mjs} +2 -2
  44. package/dist/{extract-operation-statements-DZUJNmL3.mjs.map → extract-operation-statements-DsFfxXVZ.mjs.map} +1 -1
  45. package/dist/{extract-sql-ddl-DDMX-9mz.mjs → extract-sql-ddl-D9UbZDyz.mjs} +1 -1
  46. package/dist/{extract-sql-ddl-DDMX-9mz.mjs.map → extract-sql-ddl-D9UbZDyz.mjs.map} +1 -1
  47. package/dist/{framework-components-DfZKQBQ2.mjs → framework-components-Cr--XBKy.mjs} +2 -2
  48. package/dist/{framework-components-DfZKQBQ2.mjs.map → framework-components-Cr--XBKy.mjs.map} +1 -1
  49. package/dist/init-C5220SY9.mjs +2062 -0
  50. package/dist/init-C5220SY9.mjs.map +1 -0
  51. package/dist/{inspect-live-schema-DWzf4Q_m.mjs → inspect-live-schema-yrHAvG71.mjs} +6 -6
  52. package/dist/{inspect-live-schema-DWzf4Q_m.mjs.map → inspect-live-schema-yrHAvG71.mjs.map} +1 -1
  53. package/dist/migration-cli.mjs +1 -1
  54. package/dist/{migration-command-scaffold-CLMD302g.mjs → migration-command-scaffold-B3B09et6.mjs} +6 -6
  55. package/dist/{migration-command-scaffold-CLMD302g.mjs.map → migration-command-scaffold-B3B09et6.mjs.map} +1 -1
  56. package/dist/{migration-status-B0HLF7So.mjs → migration-status-DUMiH8_G.mjs} +12 -14
  57. package/dist/{migration-status-B0HLF7So.mjs.map → migration-status-DUMiH8_G.mjs.map} +1 -1
  58. package/dist/{migrations-B0dOQlk0.mjs → migrations-Bo5WtTla.mjs} +2 -2
  59. package/dist/{migrations-B0dOQlk0.mjs.map → migrations-Bo5WtTla.mjs.map} +1 -1
  60. package/dist/output-BpcQrnnq.mjs +103 -0
  61. package/dist/output-BpcQrnnq.mjs.map +1 -0
  62. package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-DvQWB1nK.mjs} +1 -1
  63. package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DvQWB1nK.mjs.map} +1 -1
  64. package/dist/quick-reference-mongo.md +34 -13
  65. package/dist/quick-reference-postgres.md +11 -9
  66. package/dist/{result-handler-CIyu0Pdt.mjs → result-handler-Ba3zWQsI.mjs} +5 -78
  67. package/dist/result-handler-Ba3zWQsI.mjs.map +1 -0
  68. package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-C3ZLwQxK.mjs} +76 -2
  69. package/dist/terminal-ui-C3ZLwQxK.mjs.map +1 -0
  70. package/dist/{validate-contract-deps-esa-VQ0h.mjs → validate-contract-deps-B_Cs29TL.mjs} +1 -1
  71. package/dist/{validate-contract-deps-esa-VQ0h.mjs.map → validate-contract-deps-B_Cs29TL.mjs.map} +1 -1
  72. package/dist/{verify-BxiVp50b.mjs → verify-Bkycc-Tf.mjs} +2 -2
  73. package/dist/{verify-BxiVp50b.mjs.map → verify-Bkycc-Tf.mjs.map} +1 -1
  74. package/package.json +21 -16
  75. package/src/commands/init/detect-pnpm-catalog.ts +141 -0
  76. package/src/commands/init/errors.ts +254 -0
  77. package/src/commands/init/exit-codes.ts +62 -0
  78. package/src/commands/init/hygiene-gitattributes.ts +97 -0
  79. package/src/commands/init/hygiene-gitignore.ts +48 -0
  80. package/src/commands/init/hygiene-package-scripts.ts +91 -0
  81. package/src/commands/init/index.ts +112 -7
  82. package/src/commands/init/init.ts +766 -144
  83. package/src/commands/init/inputs.ts +421 -0
  84. package/src/commands/init/output.ts +147 -0
  85. package/src/commands/init/probe-db.ts +308 -0
  86. package/src/commands/init/reinit-cleanup.ts +83 -0
  87. package/src/commands/init/templates/agent-skill-mongo.md +63 -31
  88. package/src/commands/init/templates/agent-skill-postgres.md +1 -1
  89. package/src/commands/init/templates/agent-skill.ts +25 -3
  90. package/src/commands/init/templates/code-templates.ts +125 -32
  91. package/src/commands/init/templates/env.ts +80 -0
  92. package/src/commands/init/templates/quick-reference-mongo.md +34 -13
  93. package/src/commands/init/templates/quick-reference-postgres.md +11 -9
  94. package/src/commands/init/templates/quick-reference.ts +42 -3
  95. package/src/commands/init/templates/tsconfig.ts +167 -5
  96. package/src/commands/migration-apply.ts +3 -3
  97. package/src/commands/migration-ref.ts +32 -47
  98. package/src/commands/migration-status.ts +16 -21
  99. package/src/exports/init-output.ts +10 -0
  100. package/src/utils/command-helpers.ts +3 -3
  101. package/dist/contract-emit-fhNwwhkQ.mjs +0 -4
  102. package/dist/init-CQfo_4Ro.mjs +0 -430
  103. package/dist/init-CQfo_4Ro.mjs.map +0 -1
  104. package/dist/result-handler-CIyu0Pdt.mjs.map +0 -1
  105. package/dist/terminal-ui-C5k88MmW.mjs.map +0 -1
  106. /package/dist/{cli-errors-C0JhVj0c.d.mts → cli-errors-BFYgBH3L.d.mts} +0 -0
  107. /package/dist/{cli-errors-DHq6GQGu.mjs → cli-errors-Cd79vmTH.mjs} +0 -0
@@ -23,6 +23,90 @@ export function starterSchema(target: TargetId, authoring: AuthoringId): string
23
23
  return target === 'mongo' ? starterSchemaPslMongo() : starterSchemaPslPostgres();
24
24
  }
25
25
 
26
+ /**
27
+ * Renders a short authoring-appropriate schema sample (FR5.1) for embedding
28
+ * in `prisma-next.md`. Returns a complete fenced markdown code block.
29
+ *
30
+ * The sample intentionally shows just one model: it's illustrative, not
31
+ * a substitute for the full scaffolded contract file. The TS samples use
32
+ * the same outer shape as `starterSchemaTs*` (FR5.3) so a user reading
33
+ * the doc and the file side-by-side sees the same structure.
34
+ */
35
+ export function schemaSample(target: TargetId, authoring: AuthoringId): string {
36
+ if (authoring === 'typescript') {
37
+ return target === 'mongo' ? schemaSampleTsMongo() : schemaSampleTsPostgres();
38
+ }
39
+ return target === 'mongo' ? schemaSamplePslMongo() : schemaSamplePslPostgres();
40
+ }
41
+
42
+ function schemaSamplePslPostgres(): string {
43
+ return `\`\`\`prisma
44
+ model User {
45
+ id Int @id @default(autoincrement())
46
+ email String @unique
47
+ name String?
48
+ }
49
+ \`\`\``;
50
+ }
51
+
52
+ function schemaSamplePslMongo(): string {
53
+ return `\`\`\`prisma
54
+ model User {
55
+ id ObjectId @id @map("_id")
56
+ email String @unique
57
+ name String?
58
+ @@map("users")
59
+ }
60
+ \`\`\``;
61
+ }
62
+
63
+ function schemaSampleTsPostgres(): string {
64
+ return `\`\`\`typescript
65
+ import sqlFamily from '@prisma-next/family-sql/pack';
66
+ import { defineContract } from '@prisma-next/sql-contract-ts/contract-builder';
67
+ import postgresPack from '@prisma-next/target-postgres/pack';
68
+
69
+ export const contract = defineContract(
70
+ { family: sqlFamily, target: postgresPack },
71
+ ({ field, model }) => ({
72
+ models: {
73
+ User: model('User', {
74
+ fields: {
75
+ id: field.id.uuidv7(),
76
+ email: field.text().unique(),
77
+ name: field.text().optional(),
78
+ },
79
+ }),
80
+ },
81
+ }),
82
+ );
83
+ \`\`\``;
84
+ }
85
+
86
+ function schemaSampleTsMongo(): string {
87
+ return `\`\`\`typescript
88
+ import mongoFamily from '@prisma-next/family-mongo/pack';
89
+ import { defineContract } from '@prisma-next/mongo-contract-ts/contract-builder';
90
+ import mongoTarget from '@prisma-next/target-mongo/pack';
91
+
92
+ export const contract = defineContract(
93
+ { family: mongoFamily, target: mongoTarget },
94
+ ({ field, model }) => ({
95
+ models: {
96
+ User: model('User', {
97
+ collection: 'users',
98
+ fields: {
99
+ _id: field.objectId(),
100
+ email: field.string(),
101
+ name: field.string().optional(),
102
+ },
103
+ }),
104
+ },
105
+ }),
106
+ );
107
+ \`\`\``;
108
+ }
109
+
26
110
  function starterSchemaPslPostgres(): string {
27
111
  return `model User {
28
112
  id Int @id @default(autoincrement())
@@ -79,8 +163,9 @@ export const contract = defineContract(
79
163
  name: field.text().optional(),
80
164
  createdAt: field.createdAt(),
81
165
  },
82
- }).relations({
83
- posts: rel.hasMany('Post', { by: 'authorId' }),
166
+ relations: {
167
+ posts: rel.hasMany('Post', { by: 'authorId' }),
168
+ },
84
169
  }),
85
170
 
86
171
  Post: model('Post', {
@@ -91,8 +176,9 @@ export const contract = defineContract(
91
176
  authorId: field.uuid(),
92
177
  createdAt: field.createdAt(),
93
178
  },
94
- }).relations({
95
- author: rel.belongsTo('User', { from: 'authorId', to: 'id' }),
179
+ relations: {
180
+ author: rel.belongsTo('User', { from: 'authorId', to: 'id' }),
181
+ },
96
182
  }),
97
183
  },
98
184
  }),
@@ -102,36 +188,40 @@ export const contract = defineContract(
102
188
 
103
189
  function starterSchemaTsMongo(): string {
104
190
  return `import mongoFamily from '@prisma-next/family-mongo/pack';
105
- import { defineContract, field, model, rel } from '@prisma-next/mongo-contract-ts/contract-builder';
191
+ import { defineContract } from '@prisma-next/mongo-contract-ts/contract-builder';
106
192
  import mongoTarget from '@prisma-next/target-mongo/pack';
107
193
 
108
- const User = model('User', {
109
- collection: 'users',
110
- fields: {
111
- _id: field.objectId(),
112
- email: field.string(),
113
- name: field.string().optional(),
114
- },
115
- });
116
-
117
- const Post = model('Post', {
118
- collection: 'posts',
119
- fields: {
120
- _id: field.objectId(),
121
- title: field.string(),
122
- content: field.string().optional(),
123
- authorId: field.objectId(),
124
- },
125
- relations: {
126
- author: rel.belongsTo(User, { from: 'authorId', to: User.ref('_id') }),
127
- },
128
- });
194
+ export const contract = defineContract(
195
+ { family: mongoFamily, target: mongoTarget },
196
+ ({ field, model, rel }) => ({
197
+ models: {
198
+ User: model('User', {
199
+ collection: 'users',
200
+ fields: {
201
+ _id: field.objectId(),
202
+ email: field.string(),
203
+ name: field.string().optional(),
204
+ },
205
+ relations: {
206
+ posts: rel.hasMany('Post', { from: '_id', to: 'authorId' }),
207
+ },
208
+ }),
129
209
 
130
- export const contract = defineContract({
131
- family: mongoFamily,
132
- target: mongoTarget,
133
- models: { User, Post },
134
- });
210
+ Post: model('Post', {
211
+ collection: 'posts',
212
+ fields: {
213
+ _id: field.objectId(),
214
+ title: field.string(),
215
+ content: field.string().optional(),
216
+ authorId: field.objectId(),
217
+ },
218
+ relations: {
219
+ author: rel.belongsTo('User', { from: 'authorId', to: '_id' }),
220
+ },
221
+ }),
222
+ },
223
+ }),
224
+ );
135
225
  `;
136
226
  }
137
227
 
@@ -163,6 +253,9 @@ export const db = postgres<Contract>({ contractJson });
163
253
  import type { Contract } from './contract.d';
164
254
  import contractJson from './contract.json' with { type: 'json' };
165
255
 
166
- export const db = mongo<Contract>({ contractJson });
256
+ export const db = mongo<Contract>({
257
+ contractJson,
258
+ url: process.env['DATABASE_URL']!,
259
+ });
167
260
  `;
168
261
  }
@@ -0,0 +1,80 @@
1
+ import type { TargetId } from './code-templates';
2
+
3
+ /**
4
+ * The minimum supported server version for each target (FR8.1). The
5
+ * authoritative source of truth is each target package's
6
+ * `package.json#prismaNext.minServerVersion` field — this module
7
+ * mirrors those values and a workspace-level test asserts the two
8
+ * never drift (`templates/tsconfig-env.test.ts`).
9
+ *
10
+ * Bumping a value here in isolation is **not** safe: edit the
11
+ * corresponding target package's `package.json` first, then mirror
12
+ * here. The scaffold's `.env.example` (FR3.1, FR8.2) and the
13
+ * "Requirements" section of `prisma-next.md` both read from this
14
+ * constant, so a stale value lies to every freshly initialised user.
15
+ */
16
+ export const MIN_SERVER_VERSION: Record<TargetId, string> = {
17
+ postgres: '14',
18
+ mongo: '6.0',
19
+ };
20
+
21
+ export const TARGET_LABEL: Record<TargetId, string> = {
22
+ postgres: 'PostgreSQL',
23
+ mongo: 'MongoDB',
24
+ };
25
+
26
+ /**
27
+ * Renders the placeholder body shared by `.env` and `.env.example`:
28
+ * the target-specific connection-string requirement comments and the
29
+ * commented-shape `DATABASE_URL` line. The output is identical for both
30
+ * authoring styles — the env file is orthogonal to PSL vs TS schema
31
+ * authoring.
32
+ */
33
+ function envPlaceholderBody(target: TargetId): string {
34
+ const label = TARGET_LABEL[target];
35
+ const minVersion = MIN_SERVER_VERSION[target];
36
+ const lines: string[] = [];
37
+ lines.push(`# Connection string for ${label}.`);
38
+ lines.push(`# Requires ${label} >= ${minVersion}.`);
39
+ lines.push('');
40
+ if (target === 'postgres') {
41
+ lines.push('DATABASE_URL="postgresql://user:password@localhost:5432/mydb"');
42
+ } else {
43
+ lines.push('DATABASE_URL="mongodb://localhost:27017/mydb"');
44
+ }
45
+ lines.push('');
46
+ return lines.join('\n');
47
+ }
48
+
49
+ /**
50
+ * Renders the `.env.example` content for a given target (FR3.1):
51
+ *
52
+ * - Carries a "Copy this file to `.env`…" intro that only makes sense
53
+ * for the example file (the real `.env` is the destination of that
54
+ * copy and so does not get the same intro).
55
+ * - Documents the `DATABASE_URL` placeholder in the target's native URL
56
+ * shape (Postgres: standard `postgresql://`, Mongo: `mongodb://` plus
57
+ * a `mydb` database segment so the lazy facade has a `dbName`).
58
+ * - Carries a `# Requires <db> >= <version>` comment so a fresh user
59
+ * knows the minimum supported server before they first try to
60
+ * connect (FR8.2).
61
+ */
62
+ export function envExampleContent(target: TargetId): string {
63
+ const lines: string[] = [];
64
+ lines.push(
65
+ '# Copy this file to `.env` and replace the placeholder with your real connection string.',
66
+ );
67
+ lines.push(envPlaceholderBody(target));
68
+ return lines.join('\n');
69
+ }
70
+
71
+ /**
72
+ * Renders the initial `.env` content for `--write-env` / interactive
73
+ * opt-in (FR3.2). Same placeholder body as `.env.example`, **without**
74
+ * the example file's "Copy this file to `.env`…" intro: the real `.env`
75
+ * is the destination of that copy, so the line would lie. Writing this
76
+ * file is gitignored (FR3.3 ensures `.env` lands in `.gitignore`).
77
+ */
78
+ export function envFileContent(target: TargetId): string {
79
+ return envPlaceholderBody(target);
80
+ }
@@ -4,35 +4,29 @@ Prisma Next lets you query your database in simple, easy-to-read TypeScript. Def
4
4
 
5
5
  This project is set up for MongoDB. Prisma Next also supports other databases.
6
6
 
7
+ {{requirements}}
8
+
7
9
  ## Your data contract
8
10
 
9
11
  Your data contract is the heart of your application. It lives at [`{{schemaPath}}`]({{schemaPath}}) and describes your models:
10
12
 
11
- ```prisma
12
- model User {
13
- id ObjectId @id @map("_id")
14
- email String @unique
15
- name String?
16
- posts Post[]
17
- @@map("users")
18
- }
19
- ```
13
+ {{schemaSample}}
20
14
 
21
15
  Every model you define in your contract can be queried from your app. Your editor will autocomplete the query methods and show you what type each field is:
22
16
 
23
17
  ```typescript
24
18
  import { db } from '{{dbImportPath}}';
25
19
 
26
- const client = await db.connect(process.env['DATABASE_URL']!, 'mydb');
27
-
28
- const user = await client.orm.User
20
+ const user = await db.orm.users
29
21
  .where({ email: 'alice@example.com' })
30
22
  .first();
31
23
 
32
24
  // Your editor will show the type of user as
33
- // { id: ObjectId; email: string; name: string | null; posts: Post[] } | null
25
+ // { _id: ObjectId; email: string; name: string | null; posts: Post[] } | null
34
26
  ```
35
27
 
28
+ `db` connects to MongoDB lazily on the first query, so there is no manual `connect(...)` step in your application code. Call `await db.close()` if you need to release the underlying connection (typically only in tests or short-lived scripts). After `close()` the client is single-shot — any further query, `connect()`, or `runtime()` call rejects with `"Mongo client is closed"`. Construct a new `mongo({...})` if you need to use it again.
29
+
36
30
  Your contract has two companion files in the same directory:
37
31
 
38
32
  - **`contract.json`** — this tells your application what models exist, just like `package-lock.json` tells your package manager what dependencies your project has
@@ -91,3 +85,30 @@ You can customize how your environment variables are loaded by changing or remov
91
85
  1. Edit [`{{schemaPath}}`]({{schemaPath}}) to add or change models.
92
86
  2. Run `{{pkgRun}} contract emit` to regenerate the contract.
93
87
  3. Query your models — your IDE will autocomplete everything.
88
+
89
+ ## Transactions and change streams (Mongo)
90
+
91
+ Multi-document transactions and change streams require MongoDB to run as a **replica set** — even single-node setups for development. A standalone `mongod` will reject `withTransaction()` at runtime. For local development you have a few options:
92
+
93
+ - **Docker Compose:** start `mongo` with `--replSet rs0` and run `rs.initiate()` once.
94
+ - **`mongodb-memory-server`:** use `MongoMemoryReplSet` instead of `MongoMemoryServer` in tests.
95
+ - **MongoDB Atlas:** every Atlas cluster is already a replica set.
96
+
97
+ The transaction API (`db.transaction(...)`) is on the roadmap and tracked under [TML-2313](https://linear.app/prisma-company/issue/TML-2313/mongo-dev-replica-set-story-is-missing-transactions-change-streams). Prisma Next's Mongo facade does not expose it yet — until that ticket lands, drive transactions yourself with a raw `MongoClient` using the escape hatch in the next section.
98
+
99
+ ## Escape hatches
100
+
101
+ The ORM covers the common cases. For the rest, two escape hatches are designed in:
102
+
103
+ - **Typed raw aggregations — `db.query`.** The facade exposes `db.query`, a typed builder for aggregation pipelines that runs through the same runtime + middleware + codec stack as `db.orm`. Reach for it when the ORM can't express a `$lookup`/`$facet`/`$graphLookup`/window-function pipeline.
104
+ - **Direct `mongodb` driver control — `mongoClient` binding.** Construct your own `MongoClient` and pass it to `mongo({ mongoClient, dbName, contractJson })`. Your code keeps the `MongoClient` reference and uses it directly (transactions, change streams, sessions, anything Prisma Next doesn't surface yet); the same `db` object continues to give you the typed ORM.
105
+
106
+ `db.runtime()` is **not** the 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.
107
+
108
+ ## Monorepo notes (pnpm workspaces)
109
+
110
+ If this project lives inside a pnpm workspace, a few things are worth knowing:
111
+
112
+ - **Catalogs.** When the workspace's `pnpm-workspace.yaml` defines a `catalogs` entry for `prisma-next` or `@prisma-next/mongo`, pnpm uses the catalog version everywhere — `init` does too. If you wanted the published `latest` instead, update or remove the catalog entry, then re-run `pnpm install`.
113
+ - **`pnpm dlx`.** `pnpm dlx prisma-next@latest init …` works in any directory. Inside a workspace, pnpm still resolves dependencies through the workspace's catalog/overrides rather than the registry; expect the installed Prisma Next packages to reflect the workspace's catalog rather than `latest`.
114
+ - **`pnpm` → `npm` fallback.** If `pnpm` ever fails to install Prisma Next with a `workspace:*` or `catalog:` resolution error (a leak in a published artefact), `init` falls back to `npm install` and surfaces a warning. Once the offending package republishes a clean version you can switch back with `pnpm install`.
@@ -4,19 +4,13 @@ Prisma Next lets you query your database in simple, easy-to-read TypeScript. Def
4
4
 
5
5
  This project is set up for PostgreSQL. Prisma Next also supports other databases.
6
6
 
7
+ {{requirements}}
8
+
7
9
  ## Your data contract
8
10
 
9
11
  Your data contract is the heart of your application. It lives at [`{{schemaPath}}`]({{schemaPath}}) and describes your models:
10
12
 
11
- ```prisma
12
- model User {
13
- id Int @id @default(autoincrement())
14
- email String @unique
15
- name String?
16
- posts Post[]
17
- createdAt DateTime @default(now())
18
- }
19
- ```
13
+ {{schemaSample}}
20
14
 
21
15
  Every model you define in your contract can be queried from your app. Your editor will autocomplete the query methods and show you what type each model field is:
22
16
 
@@ -89,3 +83,11 @@ You can customize how your environment variables are loaded by changing or remov
89
83
  1. Edit [`{{schemaPath}}`]({{schemaPath}}) to add or change models.
90
84
  2. Run `{{pkgRun}} contract emit` to regenerate the contract.
91
85
  3. Query your models — your IDE will autocomplete everything.
86
+
87
+ ## Monorepo notes (pnpm workspaces)
88
+
89
+ If this project lives inside a pnpm workspace, a few things are worth knowing:
90
+
91
+ - **Catalogs.** When the workspace's `pnpm-workspace.yaml` defines a `catalogs` entry for `prisma-next` or `@prisma-next/postgres`, pnpm uses the catalog version everywhere — `init` does too. If you wanted the published `latest` instead, update or remove the catalog entry, then re-run `pnpm install`.
92
+ - **`pnpm dlx`.** `pnpm dlx prisma-next@latest init …` works in any directory. Inside a workspace, pnpm still resolves dependencies through the workspace's catalog/overrides rather than the registry; expect the installed Prisma Next packages to reflect the workspace's catalog rather than `latest`.
93
+ - **`pnpm` → `npm` fallback.** If `pnpm` ever fails to install Prisma Next with a `workspace:*` or `catalog:` resolution error (a leak in a published artefact), `init` falls back to `npm install` and surfaces a warning. Once the offending package republishes a clean version you can switch back with `pnpm install`.
@@ -1,19 +1,58 @@
1
1
  import { dirname } from 'pathe';
2
- import type { TargetId } from './code-templates';
2
+ import { type AuthoringId, schemaSample, type TargetId } from './code-templates';
3
+ import { MIN_SERVER_VERSION, TARGET_LABEL } from './env';
3
4
  import { renderTemplate } from './render';
4
5
 
5
- export const variables = ['schemaPath', 'schemaDir', 'dbImportPath', 'pkgRun'] as const;
6
+ export const variables = [
7
+ 'schemaPath',
8
+ 'schemaDir',
9
+ 'dbImportPath',
10
+ 'pkgRun',
11
+ 'schemaSample',
12
+ 'requirements',
13
+ ] as const;
6
14
 
7
15
  type TemplateVars = Record<(typeof variables)[number], string>;
8
16
 
9
- export function quickReferenceMd(target: TargetId, schemaPath: string, pkgRun: string): string {
17
+ export function quickReferenceMd(
18
+ target: TargetId,
19
+ authoring: AuthoringId,
20
+ schemaPath: string,
21
+ pkgRun: string,
22
+ ): string {
10
23
  const schemaDir = dirname(schemaPath);
11
24
  const vars: TemplateVars = {
12
25
  schemaPath,
13
26
  schemaDir,
14
27
  dbImportPath: `./${schemaDir}/db`,
15
28
  pkgRun,
29
+ schemaSample: schemaSample(target, authoring),
30
+ requirements: requirementsBlock(target),
16
31
  };
17
32
  const templateFile = `quick-reference-${target}.md`;
18
33
  return renderTemplate(templateFile, variables, vars);
19
34
  }
35
+
36
+ /**
37
+ * Renders the FR8.2 "Requirements" block injected into `prisma-next.md`
38
+ * (the user-facing quick reference). Sources the minimum server
39
+ * version from `MIN_SERVER_VERSION` — itself mirrored from each
40
+ * target package's `package.json#prismaNext.minServerVersion`
41
+ * (FR8.1).
42
+ *
43
+ * The verification command is target-specific — Postgres scaffolds
44
+ * shouldn't ship Mongo's `db.runCommand` (and vice versa) just because
45
+ * we couldn't be bothered to branch.
46
+ */
47
+ function requirementsBlock(target: TargetId): string {
48
+ const label = TARGET_LABEL[target];
49
+ const minVersion = MIN_SERVER_VERSION[target];
50
+ const verifyCommand =
51
+ target === 'postgres' ? '`SELECT version()`' : '`db.runCommand({ buildInfo: 1 })`';
52
+ return [
53
+ '## Requirements',
54
+ '',
55
+ `- **${label} ${minVersion} or newer.** Older servers are not supported. Run ${verifyCommand} against your server to verify.`,
56
+ '- The CLI never connects to your database without explicit consent. Pass `--probe-db` to `prisma-next init` if you want `init` to verify the server version itself.',
57
+ ].join('\n');
58
+ }
@@ -1,15 +1,48 @@
1
+ import {
2
+ applyEdits,
3
+ modify,
4
+ type ParseError,
5
+ parse as parseJsonc,
6
+ printParseErrorCode,
7
+ } from 'jsonc-parser';
8
+
9
+ /**
10
+ * Compiler options the scaffolded `prisma-next.config.ts` and `db.ts` need
11
+ * to typecheck:
12
+ *
13
+ * - `module: 'preserve'` + `moduleResolution: 'bundler'` align with how
14
+ * modern bundlers (and `tsdown`) consume our facade packages.
15
+ * - `resolveJsonModule` lets `db.ts` import `contract.json with { type:
16
+ * 'json' }` — the runtime path the facades document (FR4).
17
+ *
18
+ * `types: ['node']` is FR2.2 territory and lives in
19
+ * `REQUIRED_COMPILER_OPTIONS_TYPES` because TS only honours an _array_
20
+ * here, and a string-keyed merge would clobber any user-specified entries.
21
+ * Merge handling preserves any extra `types` the user added.
22
+ */
1
23
  export const REQUIRED_COMPILER_OPTIONS: Record<string, string | boolean> = {
2
24
  module: 'preserve',
3
25
  moduleResolution: 'bundler',
4
26
  resolveJsonModule: true,
5
27
  };
6
28
 
29
+ /**
30
+ * Types that must be present in `compilerOptions.types` for the scaffold
31
+ * to typecheck. With `moduleResolution: 'bundler'`, TypeScript does not
32
+ * implicitly include all `@types/*` packages — `process.env` only resolves
33
+ * when `node` is in this array (or `types` is omitted, but then any other
34
+ * type listed here would force the same behaviour). Listing `node`
35
+ * explicitly is the documented escape hatch (FR2.2).
36
+ */
37
+ export const REQUIRED_COMPILER_OPTIONS_TYPES: readonly string[] = ['node'];
38
+
7
39
  export function defaultTsConfig(): string {
8
40
  return JSON.stringify(
9
41
  {
10
42
  compilerOptions: {
11
43
  target: 'ES2022',
12
44
  ...REQUIRED_COMPILER_OPTIONS,
45
+ types: [...REQUIRED_COMPILER_OPTIONS_TYPES],
13
46
  strict: true,
14
47
  skipLibCheck: true,
15
48
  esModuleInterop: true,
@@ -22,14 +55,143 @@ export function defaultTsConfig(): string {
22
55
  );
23
56
  }
24
57
 
58
+ /**
59
+ * Thrown by `mergeTsConfig` when the user's existing `tsconfig.json` is
60
+ * not parseable as JSONC (TypeScript's actual configured dialect — see
61
+ * FR6.1). Carries the raw parse errors so the caller can render an
62
+ * actionable, location-aware message.
63
+ *
64
+ * `runInit` catches this exception during the precondition phase and
65
+ * maps it to a `CliStructuredError(5011)` so the user's working tree
66
+ * stays byte-identical when init bails (FR6.2 / NFR3).
67
+ */
68
+ export class TsConfigParseError extends Error {
69
+ readonly errors: readonly ParseError[];
70
+
71
+ constructor(errors: readonly ParseError[]) {
72
+ super(formatTsConfigParseErrors(errors));
73
+ this.errors = errors;
74
+ this.name = 'TsConfigParseError';
75
+ }
76
+ }
77
+
78
+ function formatTsConfigParseErrors(errors: readonly ParseError[]): string {
79
+ if (errors.length === 0) {
80
+ return 'tsconfig.json is empty or not an object';
81
+ }
82
+ return errors.map((e) => `${printParseErrorCode(e.error)} at offset ${e.offset}`).join('; ');
83
+ }
84
+
85
+ /**
86
+ * Merges the required compiler options into an existing `tsconfig.json`.
87
+ *
88
+ * Parsing is delegated to `jsonc-parser` so JSONC inputs (comments,
89
+ * trailing commas) — TypeScript's real configuration dialect — survive
90
+ * unchanged: edits are applied as text patches via `modify` /
91
+ * `applyEdits`, preserving the user's formatting, key ordering, and
92
+ * comments wherever the touched paths permit (FR6.1, AC "Hostile
93
+ * inputs").
94
+ *
95
+ * Throws `TsConfigParseError` when the input is not parseable as JSONC.
96
+ * The caller must catch this and surface a structured error before
97
+ * writing any scaffold files (FR6.2 atomicity).
98
+ */
25
99
  export function mergeTsConfig(existing: string): string {
26
- const config = JSON.parse(existing) as Record<string, unknown>;
27
- const compilerOptions = (config['compilerOptions'] ?? {}) as Record<string, unknown>;
100
+ const { config } = parseTsConfigText(existing);
28
101
 
102
+ // Match the indentation / line-ending style of the existing file so
103
+ // the merged output diffs cleanly against it. `jsonc-parser` uses
104
+ // these only when it has to insert a brand-new object node;
105
+ // existing-key edits replace the value in place.
106
+ const formattingOptions = {
107
+ tabSize: detectIndent(existing),
108
+ insertSpaces: true,
109
+ eol: existing.includes('\r\n') ? '\r\n' : '\n',
110
+ };
111
+
112
+ let result = existing;
29
113
  for (const [key, value] of Object.entries(REQUIRED_COMPILER_OPTIONS)) {
30
- compilerOptions[key] = value;
114
+ const edits = modify(result, ['compilerOptions', key], value, { formattingOptions });
115
+ result = applyEdits(result, edits);
116
+ }
117
+
118
+ const existingTypes = (config['compilerOptions'] as Record<string, unknown> | undefined)?.[
119
+ 'types'
120
+ ];
121
+ const mergedTypes = mergeTypesArray(existingTypes);
122
+ const typesEdits = modify(result, ['compilerOptions', 'types'], mergedTypes, {
123
+ formattingOptions,
124
+ });
125
+ result = applyEdits(result, typesEdits);
126
+
127
+ return result;
128
+ }
129
+
130
+ /**
131
+ * Parses an existing `tsconfig.json` (JSONC) and returns the structured
132
+ * config alongside any non-fatal parse warnings. Throws
133
+ * `TsConfigParseError` if the input cannot be parsed at all or does
134
+ * not resolve to a JSON object — both cases mean we cannot safely
135
+ * apply edits.
136
+ *
137
+ * Exposed independently so callers (notably `runInit`'s precondition
138
+ * gate) can validate the file *before* any scaffold file is written.
139
+ */
140
+ export function parseTsConfigText(text: string): {
141
+ readonly config: Record<string, unknown>;
142
+ } {
143
+ const errors: ParseError[] = [];
144
+ const value = parseJsonc(text, errors, {
145
+ allowTrailingComma: true,
146
+ disallowComments: false,
147
+ allowEmptyContent: false,
148
+ });
149
+
150
+ if (value === undefined || value === null || typeof value !== 'object' || Array.isArray(value)) {
151
+ throw new TsConfigParseError(errors);
152
+ }
153
+ if (errors.length > 0) {
154
+ throw new TsConfigParseError(errors);
155
+ }
156
+ return { config: value as Record<string, unknown> };
157
+ }
158
+
159
+ function detectIndent(text: string): number {
160
+ // Look at the first indented line. A 2-space indent (the TS default)
161
+ // is by far the most common; we fall back to 2 when nothing useful
162
+ // is detectable (e.g. a single-line tsconfig).
163
+ const match = text.match(/^([ \t]+)\S/m);
164
+ if (match === null) {
165
+ return 2;
166
+ }
167
+ const indent = match[1] ?? '';
168
+ if (indent.startsWith('\t')) {
169
+ return 1;
31
170
  }
171
+ return indent.length || 2;
172
+ }
32
173
 
33
- config['compilerOptions'] = compilerOptions;
34
- return JSON.stringify(config, null, 2);
174
+ /**
175
+ * Merges `REQUIRED_COMPILER_OPTIONS_TYPES` into the user's existing
176
+ * `compilerOptions.types` array. Preserves order and dedupes. If the
177
+ * user has no `types` array (or has set it to a non-array), we replace
178
+ * with the required minimum — overwriting a non-array `types` is the
179
+ * correct fix because anything other than a string array is invalid TS
180
+ * config.
181
+ */
182
+ function mergeTypesArray(existing: unknown): readonly string[] {
183
+ const result: string[] = [];
184
+ if (Array.isArray(existing)) {
185
+ for (const item of existing) {
186
+ if (typeof item === 'string' && !result.includes(item)) {
187
+ result.push(item);
188
+ }
189
+ }
190
+ }
191
+ for (const required of REQUIRED_COMPILER_OPTIONS_TYPES) {
192
+ if (!result.includes(required)) {
193
+ result.push(required);
194
+ }
195
+ }
196
+ return result;
35
197
  }
@@ -112,7 +112,7 @@ async function executeMigrationApplyCommand(
112
112
  startTime: number,
113
113
  ): Promise<Result<MigrationApplyResult, CliStructuredErrorType>> {
114
114
  const config = await loadConfig(options.config);
115
- const { configPath, migrationsDir, migrationsRelative, refsPath } = resolveMigrationPaths(
115
+ const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
116
116
  options.config,
117
117
  config,
118
118
  );
@@ -149,8 +149,8 @@ async function executeMigrationApplyCommand(
149
149
  if (options.ref) {
150
150
  refName = options.ref;
151
151
  try {
152
- const refs = await readRefs(refsPath);
153
- destinationHash = resolveRef(refs, refName);
152
+ const refs = await readRefs(refsDir);
153
+ destinationHash = resolveRef(refs, refName).hash;
154
154
  } catch (error) {
155
155
  if (MigrationToolsError.is(error)) {
156
156
  return notOk(mapMigrationToolsError(error));