Skip to content

Commit 4aeac05

Browse files
chore(tml-2891): merge origin/main (TML-2794 M:N PSL many-to-many)
Resolves 6 conflicts keeping both TML-2891 changes (no placeholder namespace, createNamespace required) and TML-2794 M:N additions: - validators.ts: take our pure structural validateStorage; add main's StorageColumn for validateRelationThroughConsistency; drop SqlUnboundNamespace and SqlStorageInput (not needed on this branch). - interpreter.ts: take main's new CST/symbol-table imports; keep our SqlNamespaceInput/SqlNamespace types (SqlNamespaceTablesInput deleted); drop unused Namespace import. - provider.ts: take main's buildSymbolTable/parse/rangeToPslSpan imports; keep our SqlNamespace/SqlNamespaceInput types; drop unused Namespace. - fixtures.ts: take main's SymbolTable/buildSymbolTable/parse imports; keep our createTestSqlNamespace/SqlNamespace/SqlNamespaceInput; drop buildSqlNamespace (deleted) and unused Namespace import. - composed-mutation-defaults.test.ts: import both createTestNamespace (our branch, used for createNamespace factory) and symbolTableInputFromParseArgs (main, used for parse input). - ts-psl-parity.test.ts: import both createTestNamespace and symbolTableInputFromParseArgs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Will Madden <madden@prisma.io> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
2 parents 94a7d18 + 508b9e0 commit 4aeac05

403 files changed

Lines changed: 21269 additions & 6444 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/rules/no-contract-data-patching-in-tests.mdc

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,20 @@ alwaysApply: false
88

99
## Rule
1010

11-
**Never, under any circumstances, patch raw contract data structures in tests.** Use real emitted fixtures. If a test really has to generate a contract at runtime, it may only generate it from a high-level representation made with a user-facing authoring surface (contract builder DSL, PSL) — never by writing, or worse, patching, the raw contract data structure.
11+
**Never, under any circumstances, patch or hand-author raw contract data structures in tests.** This is a HARD rule — hard as obsidian — not a guideline, suggestion, or default you may trade away under time pressure. Use real emitted fixtures. If a test really has to generate a contract at runtime, it may only generate it from a high-level representation made with a user-facing authoring surface (contract builder DSL, PSL) — never by writing, or worse, patching, the raw contract data structure.
12+
13+
A hand-built object literal that "happens" to match the contract shape is **not** a contract — it is a bag of random data that incidentally coincides with a real contract today and will diverge tomorrow given the velocity of this repo. Tests built on it are vacuous.
14+
15+
### No cast-smuggling
16+
17+
Constructing a contract value at runtime and forcing it through the type system with `as unknown as FrameworkContract<...>`, `as unknown as Contract`, `blindCast`/`castAs`, or any equivalent escape is **strictly forbidden** — that cast is the tell that you are hand-authoring raw contract data. Do not refactor unrelated `as` casts to `blindCast`/`castAs` to slip such a value past review. There is no acceptable variant of this workaround.
18+
19+
### If you cannot satisfy this rule
20+
21+
Stop. A workaround is never acceptable — it invalidates the rest of the work and is treated as a total failure of the task, not a partial success.
22+
23+
- **Working unattended:** stop and fix whatever prevents you from satisfying the rule (e.g. add the missing emitted fixture, or extend the authoring surface) before continuing.
24+
- **Working interactively:** stop and flag it to the operator.
1225

1326
## Why
1427

apps/lsp-playground/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.playground/
2+
dist/

apps/lsp-playground/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# @prisma-next/lsp-playground (private)
2+
3+
A throwaway dev playground that opens a `.psl` file in a browser
4+
[CodeMirror 6](https://codemirror.net/) editor wired to the Prisma Next
5+
language server (`prisma-next lsp --stdio`) for **live PSL diagnostics** and whole-document formatting.
6+
7+
It is a private, unpublished `apps/` package — not part of the framework build
8+
graph and exempt from `lint:deps` layering.
9+
10+
## Usage
11+
12+
```bash
13+
# 1. Build the CLI once (the bridge spawns its dist/cli.js):
14+
pnpm --filter @prisma-next/cli build
15+
16+
# 2a. Open a blank scratch schema (no file needed):
17+
psl-playground
18+
19+
# 2b. Or open an existing PSL file:
20+
psl-playground path/to/schema.psl
21+
```
22+
23+
The PSL file is **optional**. With no argument — or a path that does not yet
24+
exist — the playground opens a writable scratch schema under `.playground/`
25+
so you can start authoring immediately. Then open the printed
26+
`http://localhost:5273/` URL; parse diagnostics update live as you edit, and the header's **Format** button sends `textDocument/formatting` to the language server.
27+
28+
Everything (editor + LSP) is served on the single port `5273`.
29+
30+
### Config resolution
31+
32+
The language server identifies schema documents from `prisma-next.config.ts`
33+
(`contract.source.inputs`), discovering a document's config by walking up from
34+
the document's own path. The playground resolves what the editor opens, and the
35+
config that sits above it, as follows:
36+
37+
1. An **existing** file already inside a project (a `prisma-next.config.ts` is
38+
found walking up from it): open it in place under that config.
39+
2. Otherwise (no file, a non-existent path, or an existing file with no project
40+
config): **stage a copy** of the schema under `.playground/` and generate a
41+
**default-postgres** config beside it — the "without a config, assume default
42+
postgres" path. Staging is required because the server resolves the generated
43+
config's `@prisma-next/*` imports and discovers the config by walking up from
44+
the staged file.
45+
46+
There is no `--config` flag: the language server discovers config purely by
47+
walking up from each document, so it cannot be pointed at an arbitrary config
48+
path.
49+
50+
## How it works
51+
52+
```text
53+
CodeMirror 6 --LSP/WebSocket--> ws bridge --spawn+stdio--> node cli.js lsp --stdio
54+
(codemirror-languageserver) (vscode-ws-jsonrpc/server) (@prisma-next/language-server)
55+
```
56+
57+
- `src/bridge.ts``ws` + `vscode-ws-jsonrpc/server` (`createServerProcess` +
58+
`forward`), adapted from the TypeFox example (MIT).
59+
- `src/cli.ts` — arg parsing, config resolution, starts the bridge + a Vite dev
60+
server for the client.
61+
- `src/client/main.ts` — CodeMirror 6 editor with the `codemirror-languageserver`
62+
extension.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env node
2+
import { spawn } from 'node:child_process';
3+
import { dirname, resolve } from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
6+
// Thin launcher: run the TypeScript entrypoint through tsx so the playground
7+
// needs no build step (mirrors apps/telemetry-backend's tsx-run convention).
8+
const here = dirname(fileURLToPath(import.meta.url));
9+
const entry = resolve(here, '../src/cli.ts');
10+
const tsxBin = resolve(here, '../node_modules/.bin/tsx');
11+
12+
const child = spawn(tsxBin, [entry, ...process.argv.slice(2)], {
13+
stdio: 'inherit',
14+
env: process.env,
15+
});
16+
child.on('exit', (code) => process.exit(code ?? 0));
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
model User {
2+
id Int @id
3+
email String
4+
posts Post[]
5+

apps/lsp-playground/index.html

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>PSL Playground</title>
7+
<style>
8+
html, body { margin: 0; height: 100%; font-family: ui-sans-serif, system-ui, sans-serif; }
9+
body { display: flex; flex-direction: column; }
10+
#header {
11+
padding: 8px 12px;
12+
background: #1e1e2e;
13+
color: #cdd6f4;
14+
font-size: 13px;
15+
border-bottom: 1px solid #313244;
16+
display: flex;
17+
align-items: center;
18+
justify-content: space-between;
19+
gap: 12px;
20+
}
21+
#header .actions { display: flex; align-items: center; gap: 12px; min-width: 0; }
22+
#header .path { opacity: 0.7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
23+
#format-document {
24+
border: 1px solid #45475a;
25+
border-radius: 4px;
26+
background: #313244;
27+
color: #cdd6f4;
28+
cursor: pointer;
29+
font: inherit;
30+
padding: 3px 8px;
31+
}
32+
#format-document:hover { background: #45475a; }
33+
#editor { flex: 1; min-height: 0; }
34+
.cm-editor { height: 100%; }
35+
</style>
36+
</head>
37+
<body>
38+
<div id="header">
39+
<span>PSL Playground — live diagnostics from <code>prisma-next lsp</code></span>
40+
<div class="actions">
41+
<button type="button" id="format-document">Format</button>
42+
<span class="path" id="schema-path"></span>
43+
</div>
44+
</div>
45+
<div id="editor"></div>
46+
<script type="module" src="/src/client/main.ts"></script>
47+
</body>
48+
</html>

apps/lsp-playground/package.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "@prisma-next/lsp-playground",
3+
"version": "0.14.0",
4+
"private": true,
5+
"type": "module",
6+
"description": "Private dev playground: opens a PSL file in a browser CodeMirror editor wired to the Prisma Next language server for live diagnostics.",
7+
"bin": {
8+
"psl-playground": "./bin/psl-playground.mjs"
9+
},
10+
"scripts": {
11+
"start": "tsx src/cli.ts",
12+
"typecheck": "tsc --project tsconfig.json --noEmit",
13+
"lint": "biome check . --error-on-warnings",
14+
"lint:fix": "biome check --write .",
15+
"clean": "rm -rf dist coverage .playground"
16+
},
17+
"dependencies": {
18+
"@codemirror/autocomplete": "^6.18.6",
19+
"@codemirror/commands": "^6.8.1",
20+
"@codemirror/lint": "^6.8.5",
21+
"@codemirror/state": "^6.5.2",
22+
"@codemirror/view": "^6.38.1",
23+
"@prisma-next/adapter-postgres": "workspace:0.14.0",
24+
"@prisma-next/driver-postgres": "workspace:0.14.0",
25+
"@prisma-next/family-sql": "workspace:0.14.0",
26+
"@prisma-next/sql-contract-psl": "workspace:0.14.0",
27+
"@prisma-next/target-postgres": "workspace:0.14.0",
28+
"codemirror": "^6.0.2",
29+
"codemirror-languageserver": "^1.20.0",
30+
"vscode-ws-jsonrpc": "3.5.0",
31+
"ws": "^8.18.0"
32+
},
33+
"devDependencies": {
34+
"@prisma-next/cli": "workspace:0.14.0",
35+
"@prisma-next/tsconfig": "workspace:0.14.0",
36+
"@types/node": "catalog:",
37+
"@types/ws": "^8.5.13",
38+
"tsx": "catalog:",
39+
"typescript": "catalog:",
40+
"vite": "catalog:"
41+
},
42+
"engines": {
43+
"node": ">=24"
44+
}
45+
}

apps/lsp-playground/src/bridge.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { IncomingMessage, Server } from 'node:http';
2+
import type { Socket } from 'node:net';
3+
import type { IWebSocket } from 'vscode-ws-jsonrpc';
4+
import { WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc';
5+
import { createConnection, createServerProcess, forward } from 'vscode-ws-jsonrpc/server';
6+
import { WebSocketServer } from 'ws';
7+
8+
export interface BridgeOptions {
9+
/** Absolute path to the built CLI entry (`dist/cli.js`) to spawn. */
10+
readonly cliEntry: string;
11+
/** WebSocket path the client connects to (e.g. `/psl`). */
12+
readonly path: string;
13+
}
14+
15+
/**
16+
* Attaches an LSP WebSocket bridge to an existing HTTP server.
17+
*
18+
* The bridge does NOT own a port: it registers an `upgrade` listener on the
19+
* shared server (the same one Vite serves the editor from) and only claims
20+
* WebSocket upgrades on {@link BridgeOptions.path}, leaving Vite's own HMR
21+
* WebSocket and all HTTP requests untouched. On each accepted connection it
22+
* spawns `node <cliEntry> lsp --stdio` and forwards LSP
23+
* JSON-RPC traffic between the browser editor and that stdio process.
24+
*
25+
* Adapted from the canonical `vscode-ws-jsonrpc` example
26+
* (TypeFox/monaco-languageclient, MIT): `createServerProcess` spawns the stdio
27+
* LSP and `forward` pipes the socket <-> process connections. The server does
28+
* not depend on the client's `processId`, so the example's `initialize`
29+
* `processId` rewrite is intentionally omitted.
30+
*
31+
* Returns a disposer that detaches the bridge.
32+
*/
33+
export function attachBridge(server: Server, options: BridgeOptions): () => void {
34+
const wss = new WebSocketServer({ noServer: true });
35+
36+
const onUpgrade = (request: IncomingMessage, socket: Socket, head: Buffer): void => {
37+
const baseUrl = `http://${request.headers.host ?? 'localhost'}/`;
38+
const pathname = request.url !== undefined ? new URL(request.url, baseUrl).pathname : undefined;
39+
// Only claim our own path; ignore everything else (e.g. Vite's HMR socket)
40+
// so other upgrade listeners on the shared server still get a chance.
41+
if (pathname !== options.path) {
42+
return;
43+
}
44+
wss.handleUpgrade(request, socket, head, (webSocket) => {
45+
const ws: IWebSocket = {
46+
send: (content) =>
47+
webSocket.send(content, (error) => {
48+
// A transient send error (peer gone, socket closing) must not crash
49+
// the playground process; close the socket and let the LSP
50+
// connection tear down through the normal close path.
51+
if (error !== null && error !== undefined) {
52+
webSocket.close();
53+
}
54+
}),
55+
onMessage: (cb) => webSocket.on('message', (data) => cb(data)),
56+
onError: (cb) => webSocket.on('error', cb),
57+
onClose: (cb) => webSocket.on('close', cb),
58+
dispose: () => webSocket.close(),
59+
};
60+
if (webSocket.readyState === webSocket.OPEN) {
61+
launch(ws, options);
62+
} else {
63+
webSocket.on('open', () => launch(ws, options));
64+
}
65+
});
66+
};
67+
68+
server.on('upgrade', onUpgrade);
69+
return () => {
70+
server.off('upgrade', onUpgrade);
71+
wss.close();
72+
};
73+
}
74+
75+
function launch(socket: IWebSocket, options: BridgeOptions): void {
76+
const reader = new WebSocketMessageReader(socket);
77+
const writer = new WebSocketMessageWriter(socket);
78+
const socketConnection = createConnection(reader, writer, () => socket.dispose());
79+
const serverConnection = createServerProcess('PSL', 'node', [options.cliEntry, 'lsp', '--stdio']);
80+
if (serverConnection === undefined) {
81+
// The LSP subprocess could not be spawned. Don't leave the client hanging
82+
// on a backend-less socket: report it and close the connection.
83+
console.error(
84+
`Failed to spawn the language server (node ${options.cliEntry} lsp --stdio). Is @prisma-next/cli built?`,
85+
);
86+
socket.dispose();
87+
return;
88+
}
89+
forward(socketConnection, serverConnection);
90+
}

0 commit comments

Comments
 (0)