@suss/contract-prisma 0.33.1 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,43 +1,43 @@
1
1
  # @suss/contract-prisma
2
2
 
3
- Part of [suss](https://github.com/nimbuscloud-ai/suss), which reads both sides of every call in a repository and says where the two disagree.
3
+ Part of [suss](https://github.com/nimbuscloud-ai/suss), which reads both sides of every call in a repository and reports where the two disagree.
4
4
 
5
- Generate suss `BehavioralSummary[]` from a [Prisma](https://www.prisma.io/) schema. A `schema.prisma` file declares every table an application has and every column on it, which is the contract the code reads and writes against. This reader turns that declaration into storage boundaries, so a query touching a column nobody declared becomes a finding.
5
+ This package builds suss `BehavioralSummary[]` from a [Prisma](https://www.prisma.io/) schema. A `schema.prisma` file declares every table an application has and every column on it, and the code reads and writes against that. This reader turns the schema into storage boundaries, so a query that touches a column nobody declared becomes a finding.
6
6
 
7
7
  ## What this package reads
8
8
 
9
- `@suss/contract-prisma` parses `schema.prisma` with [`@mrleebo/prisma-ast`](https://github.com/MrLeebo/prisma-ast), a parser that does not pull in the Prisma runtime, so reading a schema never requires a generated client or a database connection.
9
+ `@suss/contract-prisma` parses `schema.prisma` with [`@mrleebo/prisma-ast`](https://github.com/MrLeebo/prisma-ast), a parser that does not pull in the Prisma runtime. Reading a schema never needs a generated client or a database connection.
10
10
 
11
- The `datasource` block decides whether anything comes out. A `provider` of `postgresql`, `postgres`, `mysql`, or `sqlite` sets the storage system on every boundary. Any other provider (MongoDB, for instance) returns an empty array, since document semantics are a different boundary kind.
11
+ The `datasource` block decides whether anything comes out. A `provider` of `postgresql`, `postgres`, `mysql` or `sqlite` sets the storage system on every boundary. Any other provider, such as MongoDB, returns an empty array, since document storage is a different kind of boundary.
12
12
 
13
13
  ## What it produces
14
14
 
15
- One `library`-kind summary per `model` and per `view`, with a storage boundary binding: `recognition: "prisma"`, the storage system from the datasource, a scope, and the model name as the container. The checker's storage pass pairs those against `interaction(class: "storage-access")` effects the adapters find in code. A monorepo with several Prisma schemas should pass a distinct `scope` per schema so pairings stay separate; it defaults to `"default"`.
15
+ One `library`-kind summary per `model` and per `view`, with a storage boundary binding: `recognition: "prisma"`, the storage system from the datasource, a scope, and the model name as the container. The checker's storage pass pairs these with the `interaction(class: "storage-access")` effects the adapters find in code. In a monorepo with several Prisma schemas, pass a different `scope` for each schema so their pairings stay separate. The default is `"default"`.
16
16
 
17
17
  The behavior is in `metadata.storageContract`:
18
18
 
19
- - `fieldSet: "exhaustive"`. A Prisma model declares every column its table has, so a column the code touches and this list leaves out is unknown rather than merely unlisted.
19
+ - `fieldSet: "exhaustive"`. A Prisma model declares every column its table has, so a column the code touches that is missing from this list is unknown, and not merely unlisted.
20
20
  - `fields`, one entry per column, with the Prisma type, whether the field is optional (`nullable`), and `primary` / `unique` from the `@id` and `@unique` field attributes. Scalar fields (`Int`, `BigInt`, `Float`, `Decimal`, `String`, `Boolean`, `DateTime`, `Json`, `Bytes`) and enum fields are columns.
21
- - Relation fields also appear, marked `derived`, because the Prisma client accepts them in an `include` or a `select` even though no column of that name exists. Leaving them out of a contract that calls itself exhaustive would report working code as reading an undeclared field. When the model stores the foreign key, the columns listed in `@relation(fields: [...])` come along as `relationKey`. Prisma allows that argument on one side of a relation only, so the other side and every implicit many-to-many have no key.
22
- - A many-to-many relation with no explicit join model gets a `joinContainer` entry instead, saying which table Prisma manages for it. See below.
23
- - A model with any relation also gets a derived `_count` entry, which is what the client exposes for relation counts.
24
- - `indexes`, from `@@index([...])` (not unique), `@@unique([...])`, and `@@id([...])` (both unique).
25
- - `physicalTable`, when `@@map("...")` gives the table a SQL name different from the model name. That is the bridge to code speaking SQL names directly, such as Drizzle's `pgTable("users")` or a raw query.
21
+ - Relation fields appear too, marked `derived`, because the Prisma client accepts them in an `include` or a `select` even though no column of that name exists. Leaving them out of a contract marked exhaustive would report working code as reading an undeclared field. When the model stores the foreign key, the columns listed in `@relation(fields: [...])` are included as `relationKey`. Prisma only allows that argument on one side of a relation, so the other side, and every implicit many-to-many, has no key.
22
+ - A many-to-many relation with no explicit join model gets a `joinContainer` entry instead, which gives the table Prisma manages for it. See below.
23
+ - A model with any relation also gets a derived `_count` entry, since the client exposes relation counts under that name.
24
+ - `indexes`, from `@@index([...])` (not unique), `@@unique([...])` and `@@id([...])` (both unique).
25
+ - `physicalTable`, when `@@map("...")` gives the table a SQL name different from the model name. That lets it pair with code that uses SQL names directly, such as Drizzle's `pgTable("users")` or a raw query.
26
26
 
27
- These summaries have no inputs and no transitions. A table is a shape the code agrees with, not a function that returns something. Confidence is `declared` at `high`, since the schema says all of this outright.
27
+ These summaries have no inputs and no transitions. A table is a set of columns the code has to agree with, and it does not return anything the way a function does. Confidence is `declared` at `high`, since the schema states all of this explicitly.
28
28
 
29
29
  ### Implicit many-to-many join tables
30
30
 
31
- Two list fields pointing at each other with neither side declaring `@relation(fields: [...])` are Prisma's implicit many-to-many: Prisma creates and manages the join table itself, and the schema never declares a model for it. This reader emits a summary for that table too, with the boundary Prisma would create: container `_Name` when the fields carry `@relation("Name")`, or `_FirstToSecond` with the two model names in alphabetical order otherwise, and two columns, `A` and `B`, keyed to whichever model sorts first and second. Each side of the relation gets `joinContainer` set to that table's own container name, so a `connect`, `disconnect` or `set` through the field counts as a write there instead of going unrecorded.
31
+ When two list fields point at each other and neither side declares `@relation(fields: [...])`, that is Prisma's implicit many-to-many. Prisma creates and manages the join table itself, and the schema never declares a model for it. This reader produces a summary for that table too, with the boundary Prisma would create. The container is `_Name` when the fields have `@relation("Name")`, and otherwise `_FirstToSecond`, with the two model names in alphabetical order. The table has two columns, `A` and `B`, keyed to whichever model sorts first and second. Each side of the relation gets `joinContainer` set to that table's container name, so a `connect`, `disconnect` or `set` through the field counts as a write there and is not lost.
32
32
 
33
- An explicit join model, the kind written out by hand to carry extra columns, already has a boundary through the ordinary model path above and gets none of this.
33
+ An explicit join model, the kind written out by hand to add extra columns, already has a boundary through the ordinary model path above, and none of this applies to it.
34
34
 
35
35
  ## What it does not read
36
36
 
37
- - **Non-relational providers.** A MongoDB schema comes back empty; document storage semantics are separate work.
38
- - **Composite types and unsupported field types.** A field whose type is neither a scalar, an enum, nor another model (`Unsupported(...)`, a Mongo composite type, or a typo) is skipped rather than guessed at.
37
+ - **Non-relational providers.** A MongoDB schema comes back empty. Document storage is separate work.
38
+ - **Composite types and unsupported field types.** A field whose type is not a scalar, an enum or another model is skipped, so the reader never guesses. That covers `Unsupported(...)`, a Mongo composite type, and a typo.
39
39
  - **Array scalar fields.** `String[]` is dropped along with relation lists, because the walk treats every array field as a relation array.
40
- - **Native type attributes.** `@db.VarChar(255)` and friends are not recorded; the field's Prisma type is what comes through.
40
+ - **Native type attributes.** `@db.VarChar(255)` and similar are not recorded. The field's Prisma type is what comes through.
41
41
  - **`generator` blocks, the datasource URL, and relation cardinality** as behavior of their own.
42
42
 
43
43
  ## Worked example
@@ -66,7 +66,7 @@ suss check summaries/db.json summaries/app.json
66
66
 
67
67
  The `User` summary declares `id` (primary), `email` (unique), a nullable `name`, the derived `posts` relation, a derived `_count`, an index on `email`, and `physicalTable: "users"`.
68
68
 
69
- Or programmatically:
69
+ Or from code:
70
70
 
71
71
  ```ts
72
72
  import { prismaSchemaFileToSummaries } from "@suss/contract-prisma";
@@ -78,7 +78,7 @@ const summaries = prismaSchemaFileToSummaries("prisma/schema.prisma", {
78
78
 
79
79
  ## Where it fits in suss
80
80
 
81
- Depends only on `@suss/behavioral-ir` (for the IR types it produces) and `@mrleebo/prisma-ast` (for parsing). It does not extract from source code and is independent of the language adapters. The Prisma pattern pack is what finds the query call sites this contract pairs against.
81
+ The package depends only on `@suss/behavioral-ir`, for the IR types it produces, and `@mrleebo/prisma-ast`, for parsing. It does not extract from source code and does not use the language adapters. The Prisma pattern pack finds the query call sites that this contract pairs with.
82
82
 
83
83
  ## More
84
84
 
package/dist/index.cjs CHANGED
@@ -31,7 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  prismaSchemaFileToSummaries: () => prismaSchemaFileToSummaries,
34
- prismaSchemaToSummaries: () => prismaSchemaToSummaries
34
+ prismaSchemaToSummaries: () => prismaSchemaToSummaries,
35
+ prismaStorageSystem: () => prismaStorageSystem
35
36
  });
36
37
  module.exports = __toCommonJS(index_exports);
37
38
  var import_node_fs = __toESM(require("fs"), 1);
@@ -60,7 +61,7 @@ function prismaSchemaToSummaries(source, options = {}) {
60
61
  const list = ast.list;
61
62
  const modelNames = /* @__PURE__ */ new Set();
62
63
  const enumNames = /* @__PURE__ */ new Set();
63
- let storageSystem = null;
64
+ const storageSystem = storageSystemIn(list);
64
65
  for (const node of list) {
65
66
  const n = node;
66
67
  if (n.type === "model" || n.type === "view") {
@@ -71,12 +72,6 @@ function prismaSchemaToSummaries(source, options = {}) {
71
72
  if (typeof n.name === "string") {
72
73
  enumNames.add(n.name);
73
74
  }
74
- } else if (n.type === "datasource") {
75
- const ds = node;
76
- const provider = readProviderString(ds);
77
- if (provider !== null && provider in PROVIDER_TO_SYSTEM) {
78
- storageSystem = PROVIDER_TO_SYSTEM[provider];
79
- }
80
75
  }
81
76
  }
82
77
  if (storageSystem === null) {
@@ -441,6 +436,25 @@ function arrayNames(value) {
441
436
  }
442
437
  return out;
443
438
  }
439
+ function prismaStorageSystem(source) {
440
+ try {
441
+ return storageSystemIn((0, import_prisma_ast.getSchema)(source).list);
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
446
+ function storageSystemIn(list) {
447
+ for (const node of list) {
448
+ if (node.type !== "datasource") {
449
+ continue;
450
+ }
451
+ const provider = readProviderString(node);
452
+ if (provider !== null && provider in PROVIDER_TO_SYSTEM) {
453
+ return PROVIDER_TO_SYSTEM[provider];
454
+ }
455
+ }
456
+ return null;
457
+ }
444
458
  function readProviderString(ds) {
445
459
  for (const a of ds.assignments) {
446
460
  if (a.key !== "provider") {
@@ -455,5 +469,6 @@ function readProviderString(ds) {
455
469
  // Annotate the CommonJS export names for ESM import in node:
456
470
  0 && (module.exports = {
457
471
  prismaSchemaFileToSummaries,
458
- prismaSchemaToSummaries
472
+ prismaSchemaToSummaries,
473
+ prismaStorageSystem
459
474
  });
package/dist/index.d.cts CHANGED
@@ -1,22 +1,39 @@
1
1
  import { BehavioralSummary } from '@suss/behavioral-ir';
2
2
 
3
+ /**
4
+ * Reads a Prisma schema into one storage summary per model and view, plus
5
+ * one per implicit many-to-many join table. The checker's storage pass
6
+ * pairs these with the `storage-access` interactions found in code.
7
+ *
8
+ * `@mrleebo/prisma-ast` parses the schema, so no Prisma runtime or
9
+ * generated client is needed. A schema whose datasource is not relational,
10
+ * such as MongoDB, comes back empty. The README lists what else is left out.
11
+ */
12
+
3
13
  interface PrismaSchemaToSummariesOptions {
4
- /** Override the source-file path recorded on each summary. */
14
+ /** The path recorded on each summary, in place of the default. */
5
15
  source?: string;
6
16
  /**
7
- * Scope identifier: defaults to `"default"` for single-schema
8
- * projects. Monorepos with multiple Prisma schemas should pass
9
- * distinct values per schema so pairings stay separate.
17
+ * Defaults to `"default"`. With several Prisma schemas in one repo, give
18
+ * each its own scope so their pairings stay separate.
10
19
  */
11
20
  scope?: string;
12
21
  }
13
22
  /**
14
- * Convert an in-memory Prisma schema source into `BehavioralSummary[]`.
23
+ * Converts Prisma schema text into summaries. Returns an empty array when
24
+ * the datasource is not relational.
15
25
  */
16
26
  declare function prismaSchemaToSummaries(source: string, options?: PrismaSchemaToSummariesOptions): BehavioralSummary[];
17
27
  /**
18
- * Convert a Prisma schema file on disk into `BehavioralSummary[]`.
28
+ * Reads a Prisma schema file and converts it. Throws when the file does
29
+ * not exist.
19
30
  */
20
31
  declare function prismaSchemaFileToSummaries(schemaPath: string, options?: PrismaSchemaToSummariesOptions): BehavioralSummary[];
32
+ type PrismaStorageSystem = "postgresql" | "mysql" | "sqlite";
33
+ /**
34
+ * The storage system the datasource provider says, or null for MongoDB or
35
+ * a file with no datasource. The client pack binds its calls with this.
36
+ */
37
+ declare function prismaStorageSystem(source: string): PrismaStorageSystem | null;
21
38
 
22
- export { type PrismaSchemaToSummariesOptions, prismaSchemaFileToSummaries, prismaSchemaToSummaries };
39
+ export { type PrismaSchemaToSummariesOptions, type PrismaStorageSystem, prismaSchemaFileToSummaries, prismaSchemaToSummaries, prismaStorageSystem };
package/dist/index.d.ts CHANGED
@@ -1,22 +1,39 @@
1
1
  import { BehavioralSummary } from '@suss/behavioral-ir';
2
2
 
3
+ /**
4
+ * Reads a Prisma schema into one storage summary per model and view, plus
5
+ * one per implicit many-to-many join table. The checker's storage pass
6
+ * pairs these with the `storage-access` interactions found in code.
7
+ *
8
+ * `@mrleebo/prisma-ast` parses the schema, so no Prisma runtime or
9
+ * generated client is needed. A schema whose datasource is not relational,
10
+ * such as MongoDB, comes back empty. The README lists what else is left out.
11
+ */
12
+
3
13
  interface PrismaSchemaToSummariesOptions {
4
- /** Override the source-file path recorded on each summary. */
14
+ /** The path recorded on each summary, in place of the default. */
5
15
  source?: string;
6
16
  /**
7
- * Scope identifier: defaults to `"default"` for single-schema
8
- * projects. Monorepos with multiple Prisma schemas should pass
9
- * distinct values per schema so pairings stay separate.
17
+ * Defaults to `"default"`. With several Prisma schemas in one repo, give
18
+ * each its own scope so their pairings stay separate.
10
19
  */
11
20
  scope?: string;
12
21
  }
13
22
  /**
14
- * Convert an in-memory Prisma schema source into `BehavioralSummary[]`.
23
+ * Converts Prisma schema text into summaries. Returns an empty array when
24
+ * the datasource is not relational.
15
25
  */
16
26
  declare function prismaSchemaToSummaries(source: string, options?: PrismaSchemaToSummariesOptions): BehavioralSummary[];
17
27
  /**
18
- * Convert a Prisma schema file on disk into `BehavioralSummary[]`.
28
+ * Reads a Prisma schema file and converts it. Throws when the file does
29
+ * not exist.
19
30
  */
20
31
  declare function prismaSchemaFileToSummaries(schemaPath: string, options?: PrismaSchemaToSummariesOptions): BehavioralSummary[];
32
+ type PrismaStorageSystem = "postgresql" | "mysql" | "sqlite";
33
+ /**
34
+ * The storage system the datasource provider says, or null for MongoDB or
35
+ * a file with no datasource. The client pack binds its calls with this.
36
+ */
37
+ declare function prismaStorageSystem(source: string): PrismaStorageSystem | null;
21
38
 
22
- export { type PrismaSchemaToSummariesOptions, prismaSchemaFileToSummaries, prismaSchemaToSummaries };
39
+ export { type PrismaSchemaToSummariesOptions, type PrismaStorageSystem, prismaSchemaFileToSummaries, prismaSchemaToSummaries, prismaStorageSystem };
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ function prismaSchemaToSummaries(source, options = {}) {
25
25
  const list = ast.list;
26
26
  const modelNames = /* @__PURE__ */ new Set();
27
27
  const enumNames = /* @__PURE__ */ new Set();
28
- let storageSystem = null;
28
+ const storageSystem = storageSystemIn(list);
29
29
  for (const node of list) {
30
30
  const n = node;
31
31
  if (n.type === "model" || n.type === "view") {
@@ -36,12 +36,6 @@ function prismaSchemaToSummaries(source, options = {}) {
36
36
  if (typeof n.name === "string") {
37
37
  enumNames.add(n.name);
38
38
  }
39
- } else if (n.type === "datasource") {
40
- const ds = node;
41
- const provider = readProviderString(ds);
42
- if (provider !== null && provider in PROVIDER_TO_SYSTEM) {
43
- storageSystem = PROVIDER_TO_SYSTEM[provider];
44
- }
45
39
  }
46
40
  }
47
41
  if (storageSystem === null) {
@@ -406,6 +400,25 @@ function arrayNames(value) {
406
400
  }
407
401
  return out;
408
402
  }
403
+ function prismaStorageSystem(source) {
404
+ try {
405
+ return storageSystemIn(getSchema(source).list);
406
+ } catch {
407
+ return null;
408
+ }
409
+ }
410
+ function storageSystemIn(list) {
411
+ for (const node of list) {
412
+ if (node.type !== "datasource") {
413
+ continue;
414
+ }
415
+ const provider = readProviderString(node);
416
+ if (provider !== null && provider in PROVIDER_TO_SYSTEM) {
417
+ return PROVIDER_TO_SYSTEM[provider];
418
+ }
419
+ }
420
+ return null;
421
+ }
409
422
  function readProviderString(ds) {
410
423
  for (const a of ds.assignments) {
411
424
  if (a.key !== "provider") {
@@ -419,5 +432,6 @@ function readProviderString(ds) {
419
432
  }
420
433
  export {
421
434
  prismaSchemaFileToSummaries,
422
- prismaSchemaToSummaries
435
+ prismaSchemaToSummaries,
436
+ prismaStorageSystem
423
437
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/contract-prisma",
3
- "version": "0.33.1",
3
+ "version": "0.34.1",
4
4
  "type": "module",
5
5
  "description": "Turn a Prisma schema into a suss contract and check it against the queries your code runs on those tables.",
6
6
  "keywords": [
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "license": "Apache-2.0",
16
16
  "author": "Nimbus Cloud AI LLC",
17
- "homepage": "https://github.com/nimbuscloud-ai/suss/tree/main/packages/contract/prisma#readme",
17
+ "homepage": "https://suss.sh",
18
18
  "repository": {
19
19
  "type": "git",
20
20
  "url": "git+https://github.com/nimbuscloud-ai/suss.git",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@mrleebo/prisma-ast": "^0.15.0",
51
- "@suss/behavioral-ir": "0.33.1"
51
+ "@suss/behavioral-ir": "0.34.1"
52
52
  },
53
53
  "devDependencies": {
54
54
  "tsup": "*",