@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 +29 -4
- package/dist/index.cjs +266 -51
- package/dist/index.d.cts +26 -4
- package/dist/index.d.ts +26 -4
- package/dist/index.js +263 -50
- package/package.json +2 -2
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
|
|
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
|
-
- **
|
|
40
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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,
|
|
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,
|
|
345
|
-
if (field.array === true) {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
446
|
+
function fieldToColumn(field, modelNames, context) {
|
|
348
447
|
const ft = field.fieldType;
|
|
349
|
-
|
|
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 (!
|
|
451
|
+
if (!PRISMA_SCALARS.has(ft) && !context.enumNames.has(ft)) {
|
|
356
452
|
return null;
|
|
357
453
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
29
|
-
*
|
|
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
|
|
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
|
|
29
|
-
*
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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,
|
|
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,
|
|
310
|
-
if (field.array === true) {
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
409
|
+
function fieldToColumn(field, modelNames, context) {
|
|
313
410
|
const ft = field.fieldType;
|
|
314
|
-
|
|
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 (!
|
|
414
|
+
if (!PRISMA_SCALARS.has(ft) && !context.enumNames.has(ft)) {
|
|
321
415
|
return null;
|
|
322
416
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
51
|
+
"@suss/behavioral-ir": "0.35.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"tsup": "*",
|