@suss/contract-prisma 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,15 +17,37 @@ One `library`-kind summary per `model` and per `view`, with a storage boundary b
17
17
  The behavior is in `metadata.storageContract`:
18
18
 
19
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
- - `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.
20
+ - `fields`, one entry per column. Scalar fields (`Int`, `BigInt`, `Float`, `Decimal`, `String`, `Boolean`, `DateTime`, `Json`, `Bytes`), enum fields, and lists of either are columns. Each column has:
21
+ - `name`, the field name the client uses, and `column`, the name `@map("...")` gives the column in the database, when that is different.
22
+ - `type`, the Prisma type, or the native type when the field has one, such as `@db.VarChar` for `String @db.VarChar(255)`. The length is left out. Every enum field has the type `enum`, and a list adds `[]`, as in `String[]`.
23
+ - `nullable`, from whether the field is optional. A list works differently, as described below.
24
+ - `primary` from `@id`, or from being listed in the model's `@@id([...])`, and `unique` from `@unique`.
25
+ - `default`, whether the database fills the column when a create leaves it out. See below.
26
+ - `foreignKey`, the table of the model a relation reaches, on each column its `@relation(fields: [...])` lists. The table is the target's `@@map` name, or the model name when it has none.
21
27
  - 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
28
  - 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
29
  - A model with any relation also gets a derived `_count` entry, since the client exposes relation counts under that name.
24
30
  - `indexes`, from `@@index([...])` (not unique), `@@unique([...])` and `@@id([...])` (both unique).
25
31
  - `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.
32
+ - `origin: "migrations"`, when a folder next to the schema has Prisma Migrate's `migration_lock.toml` in it. For a schema split over a folder, suss looks in the folder above it too. It means Prisma's own migrations made the tables. When another declaration of the same table disagrees with this one, the comparison takes this schema as the provider, unless the other one is a dump of the database.
26
33
 
27
34
  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
35
 
36
+ ### Defaults, and who fills them
37
+
38
+ `@default(...)` means different things depending on what it calls. Prisma's migrations turn some of them into a database default, and the client fills some of them in itself before it sends the insert:
39
+
40
+ | `@default` | In the migration SQL | Sent by the client on create | `default` |
41
+ |---|---|---|---|
42
+ | `autoincrement()`, `dbgenerated(...)` | `SERIAL`, `DEFAULT <expression>` | no | `true` |
43
+ | `uuid()`, `cuid()`, `nanoid()`, `ulid()` | nothing | yes | `false` |
44
+ | `now()`, a literal such as `false` or `"pending"` | `DEFAULT CURRENT_TIMESTAMP`, `DEFAULT false` | yes | `true` with migrations, otherwise left out |
45
+ | none, or only `@updatedAt` | nothing | `@updatedAt` only | `false` |
46
+
47
+ A literal or `now()` is a database default only when Prisma's migrations made the table. In a schema written to match a database something else manages, the client sends the value on every create, so the schema says nothing about whether the database has a default there. The reader leaves `default` out on those columns, and a comparison skips it.
48
+
49
+ A scalar list has the same problem with `nullable`. Prisma's migrations make a list column nullable, and the client reads a NULL there as an empty list. So a list column has `nullable: true` when Prisma's migrations made the table, and no `nullable` otherwise.
50
+
29
51
  ### Implicit many-to-many join tables
30
52
 
31
53
  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.
@@ -36,8 +58,9 @@ An explicit join model, the kind written out by hand to add extra columns, alrea
36
58
 
37
59
  - **Non-relational providers.** A MongoDB schema comes back empty. Document storage is separate work.
38
60
  - **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
- - **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 similar are not recorded. The field's Prisma type is what comes through.
61
+ - **Native type arguments and enum values.** `@db.VarChar(255)` comes through as `@db.VarChar`, and an enum field as `enum`, without its values.
62
+ - **Postgres schemas other than the default.** `@@schema("...")` is not read, so a model in another schema has its bare table name.
63
+ - **`migrations.path` in `prisma.config.ts`.** Only a migrations folder next to the schema sets `origin`.
41
64
  - **`generator` blocks, the datasource URL, and relation cardinality** as behavior of their own.
42
65
 
43
66
  ## Worked example
@@ -64,7 +87,9 @@ suss contract --from prisma prisma/schema.prisma -o summaries/db.json
64
87
  suss check summaries/db.json summaries/app.json
65
88
  ```
66
89
 
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"`.
90
+ The `User` summary declares `id` (primary, with a database default), `email` (unique), a nullable `name`, the derived `posts` relation, a derived `_count`, an index on `email`, and `physicalTable: "users"`.
91
+
92
+ A schema split over several files is read as one, the way Prisma reads it. Pass the folder, and every `.prisma` file below it is read. Pass one file, and when it is the only file in its folder with a datasource, the other `.prisma` files in that folder are read with it, since a file with no datasource cannot be a schema on its own. Each model's summary records the file the model is in, and every model gets the storage system of the one datasource.
68
93
 
69
94
  Or from code:
70
95
 
package/dist/index.cjs CHANGED
@@ -31,13 +31,16 @@ 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
+ prismaSchemaFiles: () => prismaSchemaFiles,
35
+ prismaSchemaToSummaries: () => prismaSchemaToSummaries,
36
+ prismaStorageSystem: () => prismaStorageSystem
35
37
  });
36
38
  module.exports = __toCommonJS(index_exports);
37
39
  var import_node_fs = __toESM(require("fs"), 1);
38
40
  var import_node_path = __toESM(require("path"), 1);
39
41
  var import_prisma_ast = require("@mrleebo/prisma-ast");
40
42
  var import_behavioral_ir = require("@suss/behavioral-ir");
43
+ var import_files = require("@suss/behavioral-ir/files");
41
44
  var PRISMA_SCALARS = /* @__PURE__ */ new Set([
42
45
  "Int",
43
46
  "BigInt",
@@ -56,11 +59,23 @@ var PROVIDER_TO_SYSTEM = {
56
59
  sqlite: "sqlite"
57
60
  };
58
61
  function prismaSchemaToSummaries(source, options = {}) {
59
- const ast = (0, import_prisma_ast.getSchema)(source);
60
- const list = ast.list;
62
+ return summariesOfSchema(
63
+ [{ file: options.source ?? "schema.prisma", text: source }],
64
+ options.scope ?? "default",
65
+ options.origin
66
+ );
67
+ }
68
+ function summariesOfSchema(sources, scope, origin) {
69
+ const nodes = sources.flatMap(
70
+ ({ file, text }) => (0, import_prisma_ast.getSchema)(text).list.map((node) => ({
71
+ node,
72
+ file
73
+ }))
74
+ );
75
+ const list = nodes.map(({ node }) => node);
61
76
  const modelNames = /* @__PURE__ */ new Set();
62
77
  const enumNames = /* @__PURE__ */ new Set();
63
- let storageSystem = null;
78
+ const storageSystem = storageSystemIn(list);
64
79
  for (const node of list) {
65
80
  const n = node;
66
81
  if (n.type === "model" || n.type === "view") {
@@ -71,24 +86,22 @@ function prismaSchemaToSummaries(source, options = {}) {
71
86
  if (typeof n.name === "string") {
72
87
  enumNames.add(n.name);
73
88
  }
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
89
  }
81
90
  }
82
91
  if (storageSystem === null) {
83
92
  return [];
84
93
  }
85
- const sourceFile = options.source ?? "schema.prisma";
86
- const scope = options.scope ?? "default";
87
94
  const summaries = [];
88
95
  const models = list.filter(
89
96
  (node) => node.type === "model"
90
97
  );
98
+ const fileOfModel = new Map(
99
+ nodes.map(({ node, file }) => [node.name, file])
100
+ );
91
101
  const relations = implicitManyToManyRelations(models, modelNames);
102
+ const tableOfModel = new Map(
103
+ models.map((model) => [model.name, physicalTableOf(model) ?? model.name])
104
+ );
92
105
  const joinContainerByField = /* @__PURE__ */ new Map();
93
106
  for (const relation of relations) {
94
107
  joinContainerByField.set(
@@ -100,7 +113,7 @@ function prismaSchemaToSummaries(source, options = {}) {
100
113
  relation.joinTable
101
114
  );
102
115
  }
103
- for (const node of list) {
116
+ for (const { node, file } of nodes) {
104
117
  const n = node;
105
118
  if (n.type !== "model" && n.type !== "view") {
106
119
  continue;
@@ -111,16 +124,25 @@ function prismaSchemaToSummaries(source, options = {}) {
111
124
  model,
112
125
  modelNames,
113
126
  enumNames,
127
+ tableOfModel,
114
128
  storageSystem,
115
129
  scope,
116
- sourceFile,
130
+ origin,
131
+ sourceFile: file,
117
132
  joinContainerByField
118
133
  })
119
134
  );
120
135
  }
121
136
  for (const relation of relations) {
122
137
  summaries.push(
123
- buildJoinTableSummary({ relation, storageSystem, scope, sourceFile })
138
+ buildJoinTableSummary({
139
+ relation,
140
+ tableOfModel,
141
+ storageSystem,
142
+ scope,
143
+ origin,
144
+ sourceFile: fileOfModel.get(relation.leftModel) ?? "schema.prisma"
145
+ })
124
146
  );
125
147
  }
126
148
  return summaries;
@@ -203,6 +225,14 @@ function buildJoinTableSummary(opts) {
203
225
  opts.relation.leftModel,
204
226
  opts.relation.rightModel
205
227
  ].sort();
228
+ const keyColumn = (name, model) => ({
229
+ name,
230
+ type: model,
231
+ nullable: false,
232
+ primary: true,
233
+ default: false,
234
+ foreignKey: opts.tableOfModel.get(model) ?? model
235
+ });
206
236
  return {
207
237
  kind: "library",
208
238
  location: {
@@ -227,14 +257,12 @@ function buildJoinTableSummary(opts) {
227
257
  metadata: {
228
258
  storageContract: {
229
259
  fieldSet: "exhaustive",
230
- fields: [
231
- { name: "A", type: modelA, nullable: false, primary: true },
232
- { name: "B", type: modelB, nullable: false, primary: true }
233
- ],
260
+ fields: [keyColumn("A", modelA), keyColumn("B", modelB)],
234
261
  indexes: [
235
262
  { fields: ["A", "B"], unique: true },
236
263
  { fields: ["B"], unique: false }
237
- ]
264
+ ],
265
+ ...originOf(opts.origin)
238
266
  }
239
267
  }
240
268
  };
@@ -243,21 +271,94 @@ function prismaSchemaFileToSummaries(schemaPath, options = {}) {
243
271
  if (!import_node_fs.default.existsSync(schemaPath)) {
244
272
  throw new Error(`Prisma schema not found: ${schemaPath}`);
245
273
  }
246
- const source = import_node_fs.default.readFileSync(schemaPath, "utf-8");
247
- return prismaSchemaToSummaries(source, {
248
- ...options,
249
- source: options.source ?? import_node_path.default.relative(process.cwd(), schemaPath)
250
- });
274
+ const root = options.projectRoot ?? (0, import_files.defaultProjectRoot)(schemaPath);
275
+ const files = prismaSchemaFiles(schemaPath);
276
+ const sources = files.map((file) => ({
277
+ file: options.source ?? (0, import_files.pathInProject)(file, root),
278
+ text: import_node_fs.default.readFileSync(file, "utf-8")
279
+ }));
280
+ return summariesOfSchema(
281
+ sources,
282
+ options.scope ?? "default",
283
+ options.origin ?? migrationsOriginOf(schemaPath, files)
284
+ );
285
+ }
286
+ function migrationsOriginOf(schemaPath, files) {
287
+ const folder = statOf(schemaPath)?.isDirectory() === true;
288
+ const own = folder ? import_node_path.default.resolve(schemaPath) : import_node_path.default.dirname(import_node_path.default.resolve(schemaPath));
289
+ const searched = folder || files.length > 1 ? [own, import_node_path.default.dirname(own)] : [own];
290
+ return searched.some(hasMigrationsFolder) ? "migrations" : void 0;
291
+ }
292
+ function hasMigrationsFolder(dir) {
293
+ return foldersIn(dir).some(
294
+ (folder) => import_node_fs.default.existsSync(import_node_path.default.join(dir, folder, "migration_lock.toml"))
295
+ );
296
+ }
297
+ function foldersIn(dir) {
298
+ try {
299
+ return import_node_fs.default.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
300
+ } catch {
301
+ return [];
302
+ }
303
+ }
304
+ function originOf(origin) {
305
+ return origin === void 0 ? {} : { origin };
306
+ }
307
+ function prismaSchemaFiles(schemaPath) {
308
+ const stat = statOf(schemaPath);
309
+ if (stat?.isDirectory() === true) {
310
+ return prismaFilesIn(schemaPath, true);
311
+ }
312
+ if (stat?.isFile() !== true) {
313
+ return [];
314
+ }
315
+ const beside = prismaFilesIn(import_node_path.default.dirname(schemaPath), false).flatMap(
316
+ (file) => {
317
+ const list = schemaListOf(file);
318
+ return list === null ? [] : [{ file, list }];
319
+ }
320
+ );
321
+ const datasources = beside.filter(
322
+ ({ list }) => list.some(isDatasource)
323
+ ).length;
324
+ const given = import_node_path.default.resolve(schemaPath);
325
+ return datasources === 1 && beside.some(({ file }) => import_node_path.default.resolve(file) === given) ? beside.map(({ file }) => file) : [schemaPath];
326
+ }
327
+ function statOf(file) {
328
+ try {
329
+ return import_node_fs.default.statSync(file);
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
334
+ function prismaFilesIn(dir, below) {
335
+ return import_node_fs.default.readdirSync(dir, { recursive: below, encoding: "utf8" }).filter((name) => name.endsWith(".prisma")).map((name) => import_node_path.default.join(dir, name)).filter((file) => statOf(file)?.isFile() === true).sort();
336
+ }
337
+ function schemaListOf(file) {
338
+ try {
339
+ return (0, import_prisma_ast.getSchema)(import_node_fs.default.readFileSync(file, "utf8")).list;
340
+ } catch {
341
+ return null;
342
+ }
343
+ }
344
+ function isDatasource(node) {
345
+ return node.type === "datasource";
251
346
  }
252
347
  function buildModelSummary(opts) {
253
348
  const columns = [];
254
349
  const indexes = [];
255
350
  const physicalTable = physicalTableOf(opts.model);
351
+ const context = {
352
+ enumNames: opts.enumNames,
353
+ foreignKeys: foreignKeysOf(opts.model, opts.modelNames, opts.tableOfModel),
354
+ compositeKey: compositeKeyOf(opts.model),
355
+ migrated: opts.origin === "migrations"
356
+ };
256
357
  let hasRelation = false;
257
358
  for (const property of opts.model.properties) {
258
359
  if (property.type === "field") {
259
360
  const field = property;
260
- const column = fieldToColumn(field, opts.modelNames, opts.enumNames);
361
+ const column = fieldToColumn(field, opts.modelNames, context);
261
362
  if (column !== null) {
262
363
  columns.push(column);
263
364
  }
@@ -311,7 +412,8 @@ function buildModelSummary(opts) {
311
412
  fieldSet: "exhaustive",
312
413
  fields: columns,
313
414
  indexes,
314
- ...physicalTable !== null && physicalTable !== opts.model.name ? { physicalTable } : {}
415
+ ...physicalTable !== null && physicalTable !== opts.model.name ? { physicalTable } : {},
416
+ ...originOf(opts.origin)
315
417
  }
316
418
  }
317
419
  };
@@ -341,37 +443,110 @@ function relationNameOf(field) {
341
443
  const relation = relationAttributeOf(field);
342
444
  return relation === void 0 ? null : readStringArg(relation);
343
445
  }
344
- function fieldToColumn(field, modelNames, enumNames) {
345
- if (field.array === true) {
346
- return null;
347
- }
446
+ function fieldToColumn(field, modelNames, context) {
348
447
  const ft = field.fieldType;
349
- const isScalar = PRISMA_SCALARS.has(ft);
350
- const isEnum = enumNames.has(ft);
351
- const isRelation = modelNames.has(ft);
352
- if (isRelation) {
448
+ if (modelNames.has(ft)) {
353
449
  return null;
354
450
  }
355
- if (!isScalar && !isEnum) {
451
+ if (!PRISMA_SCALARS.has(ft) && !context.enumNames.has(ft)) {
356
452
  return null;
357
453
  }
358
- let primary = false;
359
- let unique = false;
360
- for (const attr of field.attributes ?? []) {
361
- if (attr.name === "id") {
362
- primary = true;
363
- } else if (attr.name === "unique") {
364
- unique = true;
365
- }
366
- }
454
+ const column = stringArgOf(fieldAttribute(field, "map"));
455
+ const nullable = nullableOf(field, context.migrated);
456
+ const databaseDefault = databaseDefaultOf(field, context.migrated);
457
+ const foreignKey = context.foreignKeys.get(field.name);
458
+ const primary = fieldAttribute(field, "id") !== void 0 || context.compositeKey.has(field.name);
367
459
  return {
368
460
  name: field.name,
369
- type: ft,
370
- nullable: field.optional === true,
461
+ ...column !== null && column !== field.name ? { column } : {},
462
+ type: columnTypeOf(field, context.enumNames),
463
+ ...nullable === void 0 ? {} : { nullable },
371
464
  ...primary ? { primary: true } : {},
372
- ...unique ? { unique: true } : {}
465
+ ...fieldAttribute(field, "unique") !== void 0 ? { unique: true } : {},
466
+ ...databaseDefault === void 0 ? {} : { default: databaseDefault },
467
+ ...foreignKey === void 0 ? {} : { foreignKey }
373
468
  };
374
469
  }
470
+ function fieldAttribute(field, name) {
471
+ return (field.attributes ?? []).find(
472
+ (attr) => attr.name === name && attr.group === void 0
473
+ );
474
+ }
475
+ function columnTypeOf(field, enumNames) {
476
+ const element = elementTypeOf(field, enumNames);
477
+ return field.array === true ? `${element}[]` : element;
478
+ }
479
+ function elementTypeOf(field, enumNames) {
480
+ const native = (field.attributes ?? []).find((attr) => attr.group === "db");
481
+ if (native !== void 0) {
482
+ return `@db.${native.name}`;
483
+ }
484
+ return enumNames.has(field.fieldType) ? "enum" : field.fieldType;
485
+ }
486
+ function nullableOf(field, migrated) {
487
+ if (field.array === true) {
488
+ return migrated ? true : void 0;
489
+ }
490
+ return field.optional === true;
491
+ }
492
+ var DEFAULT_FUNCTION_FILLERS = {
493
+ autoincrement: "database",
494
+ dbgenerated: "database",
495
+ now: "both",
496
+ uuid: "client",
497
+ cuid: "client",
498
+ nanoid: "client",
499
+ ulid: "client"
500
+ };
501
+ var DATABASE_DEFAULT = {
502
+ nobody: () => false,
503
+ database: () => true,
504
+ client: () => false,
505
+ both: (migrated) => migrated ? true : void 0,
506
+ unknown: () => void 0
507
+ };
508
+ function databaseDefaultOf(field, migrated) {
509
+ return DATABASE_DEFAULT[defaultFillerOf(field)](migrated);
510
+ }
511
+ function defaultFillerOf(field) {
512
+ const value = (fieldAttribute(field, "default")?.args ?? []).find(
513
+ (arg) => arg.value?.type !== "keyValue"
514
+ )?.value;
515
+ if (value === void 0) {
516
+ return "nobody";
517
+ }
518
+ const call = value;
519
+ if (call?.type !== "function") {
520
+ return "both";
521
+ }
522
+ return DEFAULT_FUNCTION_FILLERS[call.name ?? ""] ?? "unknown";
523
+ }
524
+ function foreignKeysOf(model, modelNames, tableOfModel) {
525
+ const targets = /* @__PURE__ */ new Map();
526
+ for (const property of model.properties) {
527
+ if (property.type !== "field") {
528
+ continue;
529
+ }
530
+ const field = property;
531
+ if (!modelNames.has(field.fieldType)) {
532
+ continue;
533
+ }
534
+ const table = tableOfModel.get(field.fieldType) ?? field.fieldType;
535
+ for (const column of relationKeyOf(field) ?? []) {
536
+ targets.set(column, table);
537
+ }
538
+ }
539
+ return targets;
540
+ }
541
+ function compositeKeyOf(model) {
542
+ for (const property of model.properties) {
543
+ const attr = property;
544
+ if (attr.type === "attribute" && attr.name === "id") {
545
+ return new Set(blockFieldsOf(attr) ?? []);
546
+ }
547
+ }
548
+ return /* @__PURE__ */ new Set();
549
+ }
375
550
  function physicalTableOf(model) {
376
551
  for (const property of model.properties) {
377
552
  if (property.type !== "attribute") {
@@ -381,13 +556,29 @@ function physicalTableOf(model) {
381
556
  if (attr.name !== "map") {
382
557
  continue;
383
558
  }
384
- const name = readStringArg(attr);
559
+ const name = stringArgOf(attr);
385
560
  if (name !== null) {
386
561
  return name;
387
562
  }
388
563
  }
389
564
  return null;
390
565
  }
566
+ function stringArgOf(attr) {
567
+ if (attr === void 0) {
568
+ return null;
569
+ }
570
+ const positional = readStringArg(attr);
571
+ if (positional !== null) {
572
+ return positional;
573
+ }
574
+ for (const arg of attr.args ?? []) {
575
+ const value = arg.value;
576
+ if (value?.type === "keyValue" && value.key === "name" && typeof value.value === "string") {
577
+ return value.value.replace(/^"|"$/g, "");
578
+ }
579
+ }
580
+ return null;
581
+ }
391
582
  function readStringArg(attr) {
392
583
  for (const arg of attr.args ?? []) {
393
584
  const value = arg.value;
@@ -401,12 +592,15 @@ function blockAttributeToIndex(attr) {
401
592
  if (attr.name !== "index" && attr.name !== "unique" && attr.name !== "id") {
402
593
  return null;
403
594
  }
404
- const fields = readArrayArg(attr);
595
+ const fields = blockFieldsOf(attr);
405
596
  if (fields === null) {
406
597
  return null;
407
598
  }
408
599
  return { fields, unique: attr.name !== "index" };
409
600
  }
601
+ function blockFieldsOf(attr) {
602
+ return readArrayArg(attr) ?? readKeyedArrayArg(attr, "fields");
603
+ }
410
604
  function readArrayArg(attr) {
411
605
  for (const arg of attr.args ?? []) {
412
606
  const names = arrayNames(arg.value);
@@ -441,6 +635,25 @@ function arrayNames(value) {
441
635
  }
442
636
  return out;
443
637
  }
638
+ function prismaStorageSystem(source) {
639
+ try {
640
+ return storageSystemIn((0, import_prisma_ast.getSchema)(source).list);
641
+ } catch {
642
+ return null;
643
+ }
644
+ }
645
+ function storageSystemIn(list) {
646
+ for (const node of list) {
647
+ if (node.type !== "datasource") {
648
+ continue;
649
+ }
650
+ const provider = readProviderString(node);
651
+ if (provider !== null && provider in PROVIDER_TO_SYSTEM) {
652
+ return PROVIDER_TO_SYSTEM[provider];
653
+ }
654
+ }
655
+ return null;
656
+ }
444
657
  function readProviderString(ds) {
445
658
  for (const a of ds.assignments) {
446
659
  if (a.key !== "provider") {
@@ -455,5 +668,7 @@ function readProviderString(ds) {
455
668
  // Annotate the CommonJS export names for ESM import in node:
456
669
  0 && (module.exports = {
457
670
  prismaSchemaFileToSummaries,
458
- prismaSchemaToSummaries
671
+ prismaSchemaFiles,
672
+ prismaSchemaToSummaries,
673
+ prismaStorageSystem
459
674
  });
package/dist/index.d.cts CHANGED
@@ -18,6 +18,12 @@ interface PrismaSchemaToSummariesOptions {
18
18
  * each its own scope so their pairings stay separate.
19
19
  */
20
20
  scope?: string;
21
+ /** `"migrations"` when Prisma's own migrations made the database. */
22
+ origin?: "migrations";
23
+ }
24
+ interface PrismaSchemaFileToSummariesOptions extends PrismaSchemaToSummariesOptions {
25
+ /** Absolute. The directory each schema file's path is measured from. */
26
+ projectRoot?: string;
21
27
  }
22
28
  /**
23
29
  * Converts Prisma schema text into summaries. Returns an empty array when
@@ -25,9 +31,25 @@ interface PrismaSchemaToSummariesOptions {
25
31
  */
26
32
  declare function prismaSchemaToSummaries(source: string, options?: PrismaSchemaToSummariesOptions): BehavioralSummary[];
27
33
  /**
28
- * Reads a Prisma schema file and converts it. Throws when the file does
29
- * not exist.
34
+ * Reads a Prisma schema from disk and converts it: a file, a schema
35
+ * folder, or one file of a multi-file schema, which brings the rest of
36
+ * its files with it. Throws when the path does not exist. Unless the
37
+ * options say otherwise, the origin is `"migrations"` when a Prisma
38
+ * migrations folder is next to the schema.
39
+ */
40
+ declare function prismaSchemaFileToSummaries(schemaPath: string, options?: PrismaSchemaFileToSummariesOptions): BehavioralSummary[];
41
+ /**
42
+ * The files Prisma reads as one schema, given a schema file or folder.
43
+ * Prisma reads every `.prisma` file below a folder. A file with no
44
+ * datasource cannot be a schema on its own, so when only one file in a
45
+ * folder has the datasource, the folder's files are one schema.
46
+ */
47
+ declare function prismaSchemaFiles(schemaPath: string): string[];
48
+ type PrismaStorageSystem = "postgresql" | "mysql" | "sqlite";
49
+ /**
50
+ * The storage system the datasource provider says, or null for MongoDB or
51
+ * a file with no datasource. The client pack binds its calls with this.
30
52
  */
31
- declare function prismaSchemaFileToSummaries(schemaPath: string, options?: PrismaSchemaToSummariesOptions): BehavioralSummary[];
53
+ declare function prismaStorageSystem(source: string): PrismaStorageSystem | null;
32
54
 
33
- export { type PrismaSchemaToSummariesOptions, prismaSchemaFileToSummaries, prismaSchemaToSummaries };
55
+ export { type PrismaSchemaFileToSummariesOptions, type PrismaSchemaToSummariesOptions, type PrismaStorageSystem, prismaSchemaFileToSummaries, prismaSchemaFiles, prismaSchemaToSummaries, prismaStorageSystem };
package/dist/index.d.ts CHANGED
@@ -18,6 +18,12 @@ interface PrismaSchemaToSummariesOptions {
18
18
  * each its own scope so their pairings stay separate.
19
19
  */
20
20
  scope?: string;
21
+ /** `"migrations"` when Prisma's own migrations made the database. */
22
+ origin?: "migrations";
23
+ }
24
+ interface PrismaSchemaFileToSummariesOptions extends PrismaSchemaToSummariesOptions {
25
+ /** Absolute. The directory each schema file's path is measured from. */
26
+ projectRoot?: string;
21
27
  }
22
28
  /**
23
29
  * Converts Prisma schema text into summaries. Returns an empty array when
@@ -25,9 +31,25 @@ interface PrismaSchemaToSummariesOptions {
25
31
  */
26
32
  declare function prismaSchemaToSummaries(source: string, options?: PrismaSchemaToSummariesOptions): BehavioralSummary[];
27
33
  /**
28
- * Reads a Prisma schema file and converts it. Throws when the file does
29
- * not exist.
34
+ * Reads a Prisma schema from disk and converts it: a file, a schema
35
+ * folder, or one file of a multi-file schema, which brings the rest of
36
+ * its files with it. Throws when the path does not exist. Unless the
37
+ * options say otherwise, the origin is `"migrations"` when a Prisma
38
+ * migrations folder is next to the schema.
39
+ */
40
+ declare function prismaSchemaFileToSummaries(schemaPath: string, options?: PrismaSchemaFileToSummariesOptions): BehavioralSummary[];
41
+ /**
42
+ * The files Prisma reads as one schema, given a schema file or folder.
43
+ * Prisma reads every `.prisma` file below a folder. A file with no
44
+ * datasource cannot be a schema on its own, so when only one file in a
45
+ * folder has the datasource, the folder's files are one schema.
46
+ */
47
+ declare function prismaSchemaFiles(schemaPath: string): string[];
48
+ type PrismaStorageSystem = "postgresql" | "mysql" | "sqlite";
49
+ /**
50
+ * The storage system the datasource provider says, or null for MongoDB or
51
+ * a file with no datasource. The client pack binds its calls with this.
30
52
  */
31
- declare function prismaSchemaFileToSummaries(schemaPath: string, options?: PrismaSchemaToSummariesOptions): BehavioralSummary[];
53
+ declare function prismaStorageSystem(source: string): PrismaStorageSystem | null;
32
54
 
33
- export { type PrismaSchemaToSummariesOptions, prismaSchemaFileToSummaries, prismaSchemaToSummaries };
55
+ export { type PrismaSchemaFileToSummariesOptions, type PrismaSchemaToSummariesOptions, type PrismaStorageSystem, prismaSchemaFileToSummaries, prismaSchemaFiles, prismaSchemaToSummaries, prismaStorageSystem };
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import fs from "fs";
3
3
  import path from "path";
4
4
  import { getSchema } from "@mrleebo/prisma-ast";
5
5
  import { storageBinding } from "@suss/behavioral-ir";
6
+ import { defaultProjectRoot, pathInProject } from "@suss/behavioral-ir/files";
6
7
  var PRISMA_SCALARS = /* @__PURE__ */ new Set([
7
8
  "Int",
8
9
  "BigInt",
@@ -21,11 +22,23 @@ var PROVIDER_TO_SYSTEM = {
21
22
  sqlite: "sqlite"
22
23
  };
23
24
  function prismaSchemaToSummaries(source, options = {}) {
24
- const ast = getSchema(source);
25
- const list = ast.list;
25
+ return summariesOfSchema(
26
+ [{ file: options.source ?? "schema.prisma", text: source }],
27
+ options.scope ?? "default",
28
+ options.origin
29
+ );
30
+ }
31
+ function summariesOfSchema(sources, scope, origin) {
32
+ const nodes = sources.flatMap(
33
+ ({ file, text }) => getSchema(text).list.map((node) => ({
34
+ node,
35
+ file
36
+ }))
37
+ );
38
+ const list = nodes.map(({ node }) => node);
26
39
  const modelNames = /* @__PURE__ */ new Set();
27
40
  const enumNames = /* @__PURE__ */ new Set();
28
- let storageSystem = null;
41
+ const storageSystem = storageSystemIn(list);
29
42
  for (const node of list) {
30
43
  const n = node;
31
44
  if (n.type === "model" || n.type === "view") {
@@ -36,24 +49,22 @@ function prismaSchemaToSummaries(source, options = {}) {
36
49
  if (typeof n.name === "string") {
37
50
  enumNames.add(n.name);
38
51
  }
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
52
  }
46
53
  }
47
54
  if (storageSystem === null) {
48
55
  return [];
49
56
  }
50
- const sourceFile = options.source ?? "schema.prisma";
51
- const scope = options.scope ?? "default";
52
57
  const summaries = [];
53
58
  const models = list.filter(
54
59
  (node) => node.type === "model"
55
60
  );
61
+ const fileOfModel = new Map(
62
+ nodes.map(({ node, file }) => [node.name, file])
63
+ );
56
64
  const relations = implicitManyToManyRelations(models, modelNames);
65
+ const tableOfModel = new Map(
66
+ models.map((model) => [model.name, physicalTableOf(model) ?? model.name])
67
+ );
57
68
  const joinContainerByField = /* @__PURE__ */ new Map();
58
69
  for (const relation of relations) {
59
70
  joinContainerByField.set(
@@ -65,7 +76,7 @@ function prismaSchemaToSummaries(source, options = {}) {
65
76
  relation.joinTable
66
77
  );
67
78
  }
68
- for (const node of list) {
79
+ for (const { node, file } of nodes) {
69
80
  const n = node;
70
81
  if (n.type !== "model" && n.type !== "view") {
71
82
  continue;
@@ -76,16 +87,25 @@ function prismaSchemaToSummaries(source, options = {}) {
76
87
  model,
77
88
  modelNames,
78
89
  enumNames,
90
+ tableOfModel,
79
91
  storageSystem,
80
92
  scope,
81
- sourceFile,
93
+ origin,
94
+ sourceFile: file,
82
95
  joinContainerByField
83
96
  })
84
97
  );
85
98
  }
86
99
  for (const relation of relations) {
87
100
  summaries.push(
88
- buildJoinTableSummary({ relation, storageSystem, scope, sourceFile })
101
+ buildJoinTableSummary({
102
+ relation,
103
+ tableOfModel,
104
+ storageSystem,
105
+ scope,
106
+ origin,
107
+ sourceFile: fileOfModel.get(relation.leftModel) ?? "schema.prisma"
108
+ })
89
109
  );
90
110
  }
91
111
  return summaries;
@@ -168,6 +188,14 @@ function buildJoinTableSummary(opts) {
168
188
  opts.relation.leftModel,
169
189
  opts.relation.rightModel
170
190
  ].sort();
191
+ const keyColumn = (name, model) => ({
192
+ name,
193
+ type: model,
194
+ nullable: false,
195
+ primary: true,
196
+ default: false,
197
+ foreignKey: opts.tableOfModel.get(model) ?? model
198
+ });
171
199
  return {
172
200
  kind: "library",
173
201
  location: {
@@ -192,14 +220,12 @@ function buildJoinTableSummary(opts) {
192
220
  metadata: {
193
221
  storageContract: {
194
222
  fieldSet: "exhaustive",
195
- fields: [
196
- { name: "A", type: modelA, nullable: false, primary: true },
197
- { name: "B", type: modelB, nullable: false, primary: true }
198
- ],
223
+ fields: [keyColumn("A", modelA), keyColumn("B", modelB)],
199
224
  indexes: [
200
225
  { fields: ["A", "B"], unique: true },
201
226
  { fields: ["B"], unique: false }
202
- ]
227
+ ],
228
+ ...originOf(opts.origin)
203
229
  }
204
230
  }
205
231
  };
@@ -208,21 +234,94 @@ function prismaSchemaFileToSummaries(schemaPath, options = {}) {
208
234
  if (!fs.existsSync(schemaPath)) {
209
235
  throw new Error(`Prisma schema not found: ${schemaPath}`);
210
236
  }
211
- const source = fs.readFileSync(schemaPath, "utf-8");
212
- return prismaSchemaToSummaries(source, {
213
- ...options,
214
- source: options.source ?? path.relative(process.cwd(), schemaPath)
215
- });
237
+ const root = options.projectRoot ?? defaultProjectRoot(schemaPath);
238
+ const files = prismaSchemaFiles(schemaPath);
239
+ const sources = files.map((file) => ({
240
+ file: options.source ?? pathInProject(file, root),
241
+ text: fs.readFileSync(file, "utf-8")
242
+ }));
243
+ return summariesOfSchema(
244
+ sources,
245
+ options.scope ?? "default",
246
+ options.origin ?? migrationsOriginOf(schemaPath, files)
247
+ );
248
+ }
249
+ function migrationsOriginOf(schemaPath, files) {
250
+ const folder = statOf(schemaPath)?.isDirectory() === true;
251
+ const own = folder ? path.resolve(schemaPath) : path.dirname(path.resolve(schemaPath));
252
+ const searched = folder || files.length > 1 ? [own, path.dirname(own)] : [own];
253
+ return searched.some(hasMigrationsFolder) ? "migrations" : void 0;
254
+ }
255
+ function hasMigrationsFolder(dir) {
256
+ return foldersIn(dir).some(
257
+ (folder) => fs.existsSync(path.join(dir, folder, "migration_lock.toml"))
258
+ );
259
+ }
260
+ function foldersIn(dir) {
261
+ try {
262
+ return fs.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
263
+ } catch {
264
+ return [];
265
+ }
266
+ }
267
+ function originOf(origin) {
268
+ return origin === void 0 ? {} : { origin };
269
+ }
270
+ function prismaSchemaFiles(schemaPath) {
271
+ const stat = statOf(schemaPath);
272
+ if (stat?.isDirectory() === true) {
273
+ return prismaFilesIn(schemaPath, true);
274
+ }
275
+ if (stat?.isFile() !== true) {
276
+ return [];
277
+ }
278
+ const beside = prismaFilesIn(path.dirname(schemaPath), false).flatMap(
279
+ (file) => {
280
+ const list = schemaListOf(file);
281
+ return list === null ? [] : [{ file, list }];
282
+ }
283
+ );
284
+ const datasources = beside.filter(
285
+ ({ list }) => list.some(isDatasource)
286
+ ).length;
287
+ const given = path.resolve(schemaPath);
288
+ return datasources === 1 && beside.some(({ file }) => path.resolve(file) === given) ? beside.map(({ file }) => file) : [schemaPath];
289
+ }
290
+ function statOf(file) {
291
+ try {
292
+ return fs.statSync(file);
293
+ } catch {
294
+ return null;
295
+ }
296
+ }
297
+ function prismaFilesIn(dir, below) {
298
+ return fs.readdirSync(dir, { recursive: below, encoding: "utf8" }).filter((name) => name.endsWith(".prisma")).map((name) => path.join(dir, name)).filter((file) => statOf(file)?.isFile() === true).sort();
299
+ }
300
+ function schemaListOf(file) {
301
+ try {
302
+ return getSchema(fs.readFileSync(file, "utf8")).list;
303
+ } catch {
304
+ return null;
305
+ }
306
+ }
307
+ function isDatasource(node) {
308
+ return node.type === "datasource";
216
309
  }
217
310
  function buildModelSummary(opts) {
218
311
  const columns = [];
219
312
  const indexes = [];
220
313
  const physicalTable = physicalTableOf(opts.model);
314
+ const context = {
315
+ enumNames: opts.enumNames,
316
+ foreignKeys: foreignKeysOf(opts.model, opts.modelNames, opts.tableOfModel),
317
+ compositeKey: compositeKeyOf(opts.model),
318
+ migrated: opts.origin === "migrations"
319
+ };
221
320
  let hasRelation = false;
222
321
  for (const property of opts.model.properties) {
223
322
  if (property.type === "field") {
224
323
  const field = property;
225
- const column = fieldToColumn(field, opts.modelNames, opts.enumNames);
324
+ const column = fieldToColumn(field, opts.modelNames, context);
226
325
  if (column !== null) {
227
326
  columns.push(column);
228
327
  }
@@ -276,7 +375,8 @@ function buildModelSummary(opts) {
276
375
  fieldSet: "exhaustive",
277
376
  fields: columns,
278
377
  indexes,
279
- ...physicalTable !== null && physicalTable !== opts.model.name ? { physicalTable } : {}
378
+ ...physicalTable !== null && physicalTable !== opts.model.name ? { physicalTable } : {},
379
+ ...originOf(opts.origin)
280
380
  }
281
381
  }
282
382
  };
@@ -306,37 +406,110 @@ function relationNameOf(field) {
306
406
  const relation = relationAttributeOf(field);
307
407
  return relation === void 0 ? null : readStringArg(relation);
308
408
  }
309
- function fieldToColumn(field, modelNames, enumNames) {
310
- if (field.array === true) {
311
- return null;
312
- }
409
+ function fieldToColumn(field, modelNames, context) {
313
410
  const ft = field.fieldType;
314
- const isScalar = PRISMA_SCALARS.has(ft);
315
- const isEnum = enumNames.has(ft);
316
- const isRelation = modelNames.has(ft);
317
- if (isRelation) {
411
+ if (modelNames.has(ft)) {
318
412
  return null;
319
413
  }
320
- if (!isScalar && !isEnum) {
414
+ if (!PRISMA_SCALARS.has(ft) && !context.enumNames.has(ft)) {
321
415
  return null;
322
416
  }
323
- let primary = false;
324
- let unique = false;
325
- for (const attr of field.attributes ?? []) {
326
- if (attr.name === "id") {
327
- primary = true;
328
- } else if (attr.name === "unique") {
329
- unique = true;
330
- }
331
- }
417
+ const column = stringArgOf(fieldAttribute(field, "map"));
418
+ const nullable = nullableOf(field, context.migrated);
419
+ const databaseDefault = databaseDefaultOf(field, context.migrated);
420
+ const foreignKey = context.foreignKeys.get(field.name);
421
+ const primary = fieldAttribute(field, "id") !== void 0 || context.compositeKey.has(field.name);
332
422
  return {
333
423
  name: field.name,
334
- type: ft,
335
- nullable: field.optional === true,
424
+ ...column !== null && column !== field.name ? { column } : {},
425
+ type: columnTypeOf(field, context.enumNames),
426
+ ...nullable === void 0 ? {} : { nullable },
336
427
  ...primary ? { primary: true } : {},
337
- ...unique ? { unique: true } : {}
428
+ ...fieldAttribute(field, "unique") !== void 0 ? { unique: true } : {},
429
+ ...databaseDefault === void 0 ? {} : { default: databaseDefault },
430
+ ...foreignKey === void 0 ? {} : { foreignKey }
338
431
  };
339
432
  }
433
+ function fieldAttribute(field, name) {
434
+ return (field.attributes ?? []).find(
435
+ (attr) => attr.name === name && attr.group === void 0
436
+ );
437
+ }
438
+ function columnTypeOf(field, enumNames) {
439
+ const element = elementTypeOf(field, enumNames);
440
+ return field.array === true ? `${element}[]` : element;
441
+ }
442
+ function elementTypeOf(field, enumNames) {
443
+ const native = (field.attributes ?? []).find((attr) => attr.group === "db");
444
+ if (native !== void 0) {
445
+ return `@db.${native.name}`;
446
+ }
447
+ return enumNames.has(field.fieldType) ? "enum" : field.fieldType;
448
+ }
449
+ function nullableOf(field, migrated) {
450
+ if (field.array === true) {
451
+ return migrated ? true : void 0;
452
+ }
453
+ return field.optional === true;
454
+ }
455
+ var DEFAULT_FUNCTION_FILLERS = {
456
+ autoincrement: "database",
457
+ dbgenerated: "database",
458
+ now: "both",
459
+ uuid: "client",
460
+ cuid: "client",
461
+ nanoid: "client",
462
+ ulid: "client"
463
+ };
464
+ var DATABASE_DEFAULT = {
465
+ nobody: () => false,
466
+ database: () => true,
467
+ client: () => false,
468
+ both: (migrated) => migrated ? true : void 0,
469
+ unknown: () => void 0
470
+ };
471
+ function databaseDefaultOf(field, migrated) {
472
+ return DATABASE_DEFAULT[defaultFillerOf(field)](migrated);
473
+ }
474
+ function defaultFillerOf(field) {
475
+ const value = (fieldAttribute(field, "default")?.args ?? []).find(
476
+ (arg) => arg.value?.type !== "keyValue"
477
+ )?.value;
478
+ if (value === void 0) {
479
+ return "nobody";
480
+ }
481
+ const call = value;
482
+ if (call?.type !== "function") {
483
+ return "both";
484
+ }
485
+ return DEFAULT_FUNCTION_FILLERS[call.name ?? ""] ?? "unknown";
486
+ }
487
+ function foreignKeysOf(model, modelNames, tableOfModel) {
488
+ const targets = /* @__PURE__ */ new Map();
489
+ for (const property of model.properties) {
490
+ if (property.type !== "field") {
491
+ continue;
492
+ }
493
+ const field = property;
494
+ if (!modelNames.has(field.fieldType)) {
495
+ continue;
496
+ }
497
+ const table = tableOfModel.get(field.fieldType) ?? field.fieldType;
498
+ for (const column of relationKeyOf(field) ?? []) {
499
+ targets.set(column, table);
500
+ }
501
+ }
502
+ return targets;
503
+ }
504
+ function compositeKeyOf(model) {
505
+ for (const property of model.properties) {
506
+ const attr = property;
507
+ if (attr.type === "attribute" && attr.name === "id") {
508
+ return new Set(blockFieldsOf(attr) ?? []);
509
+ }
510
+ }
511
+ return /* @__PURE__ */ new Set();
512
+ }
340
513
  function physicalTableOf(model) {
341
514
  for (const property of model.properties) {
342
515
  if (property.type !== "attribute") {
@@ -346,13 +519,29 @@ function physicalTableOf(model) {
346
519
  if (attr.name !== "map") {
347
520
  continue;
348
521
  }
349
- const name = readStringArg(attr);
522
+ const name = stringArgOf(attr);
350
523
  if (name !== null) {
351
524
  return name;
352
525
  }
353
526
  }
354
527
  return null;
355
528
  }
529
+ function stringArgOf(attr) {
530
+ if (attr === void 0) {
531
+ return null;
532
+ }
533
+ const positional = readStringArg(attr);
534
+ if (positional !== null) {
535
+ return positional;
536
+ }
537
+ for (const arg of attr.args ?? []) {
538
+ const value = arg.value;
539
+ if (value?.type === "keyValue" && value.key === "name" && typeof value.value === "string") {
540
+ return value.value.replace(/^"|"$/g, "");
541
+ }
542
+ }
543
+ return null;
544
+ }
356
545
  function readStringArg(attr) {
357
546
  for (const arg of attr.args ?? []) {
358
547
  const value = arg.value;
@@ -366,12 +555,15 @@ function blockAttributeToIndex(attr) {
366
555
  if (attr.name !== "index" && attr.name !== "unique" && attr.name !== "id") {
367
556
  return null;
368
557
  }
369
- const fields = readArrayArg(attr);
558
+ const fields = blockFieldsOf(attr);
370
559
  if (fields === null) {
371
560
  return null;
372
561
  }
373
562
  return { fields, unique: attr.name !== "index" };
374
563
  }
564
+ function blockFieldsOf(attr) {
565
+ return readArrayArg(attr) ?? readKeyedArrayArg(attr, "fields");
566
+ }
375
567
  function readArrayArg(attr) {
376
568
  for (const arg of attr.args ?? []) {
377
569
  const names = arrayNames(arg.value);
@@ -406,6 +598,25 @@ function arrayNames(value) {
406
598
  }
407
599
  return out;
408
600
  }
601
+ function prismaStorageSystem(source) {
602
+ try {
603
+ return storageSystemIn(getSchema(source).list);
604
+ } catch {
605
+ return null;
606
+ }
607
+ }
608
+ function storageSystemIn(list) {
609
+ for (const node of list) {
610
+ if (node.type !== "datasource") {
611
+ continue;
612
+ }
613
+ const provider = readProviderString(node);
614
+ if (provider !== null && provider in PROVIDER_TO_SYSTEM) {
615
+ return PROVIDER_TO_SYSTEM[provider];
616
+ }
617
+ }
618
+ return null;
619
+ }
409
620
  function readProviderString(ds) {
410
621
  for (const a of ds.assignments) {
411
622
  if (a.key !== "provider") {
@@ -419,5 +630,7 @@ function readProviderString(ds) {
419
630
  }
420
631
  export {
421
632
  prismaSchemaFileToSummaries,
422
- prismaSchemaToSummaries
633
+ prismaSchemaFiles,
634
+ prismaSchemaToSummaries,
635
+ prismaStorageSystem
423
636
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/contract-prisma",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
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": [
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@mrleebo/prisma-ast": "^0.15.0",
51
- "@suss/behavioral-ir": "0.34.0"
51
+ "@suss/behavioral-ir": "0.35.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "tsup": "*",