@suss/contract-prisma 0.34.1 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -4
- package/dist/index.cjs +242 -42
- package/dist/index.d.cts +20 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.js +241 -42
- 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,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
prismaSchemaFileToSummaries: () => prismaSchemaFileToSummaries,
|
|
34
|
+
prismaSchemaFiles: () => prismaSchemaFiles,
|
|
34
35
|
prismaSchemaToSummaries: () => prismaSchemaToSummaries,
|
|
35
36
|
prismaStorageSystem: () => prismaStorageSystem
|
|
36
37
|
});
|
|
@@ -39,6 +40,7 @@ var import_node_fs = __toESM(require("fs"), 1);
|
|
|
39
40
|
var import_node_path = __toESM(require("path"), 1);
|
|
40
41
|
var import_prisma_ast = require("@mrleebo/prisma-ast");
|
|
41
42
|
var import_behavioral_ir = require("@suss/behavioral-ir");
|
|
43
|
+
var import_files = require("@suss/behavioral-ir/files");
|
|
42
44
|
var PRISMA_SCALARS = /* @__PURE__ */ new Set([
|
|
43
45
|
"Int",
|
|
44
46
|
"BigInt",
|
|
@@ -57,8 +59,20 @@ var PROVIDER_TO_SYSTEM = {
|
|
|
57
59
|
sqlite: "sqlite"
|
|
58
60
|
};
|
|
59
61
|
function prismaSchemaToSummaries(source, options = {}) {
|
|
60
|
-
|
|
61
|
-
|
|
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);
|
|
62
76
|
const modelNames = /* @__PURE__ */ new Set();
|
|
63
77
|
const enumNames = /* @__PURE__ */ new Set();
|
|
64
78
|
const storageSystem = storageSystemIn(list);
|
|
@@ -77,13 +91,17 @@ function prismaSchemaToSummaries(source, options = {}) {
|
|
|
77
91
|
if (storageSystem === null) {
|
|
78
92
|
return [];
|
|
79
93
|
}
|
|
80
|
-
const sourceFile = options.source ?? "schema.prisma";
|
|
81
|
-
const scope = options.scope ?? "default";
|
|
82
94
|
const summaries = [];
|
|
83
95
|
const models = list.filter(
|
|
84
96
|
(node) => node.type === "model"
|
|
85
97
|
);
|
|
98
|
+
const fileOfModel = new Map(
|
|
99
|
+
nodes.map(({ node, file }) => [node.name, file])
|
|
100
|
+
);
|
|
86
101
|
const relations = implicitManyToManyRelations(models, modelNames);
|
|
102
|
+
const tableOfModel = new Map(
|
|
103
|
+
models.map((model) => [model.name, physicalTableOf(model) ?? model.name])
|
|
104
|
+
);
|
|
87
105
|
const joinContainerByField = /* @__PURE__ */ new Map();
|
|
88
106
|
for (const relation of relations) {
|
|
89
107
|
joinContainerByField.set(
|
|
@@ -95,7 +113,7 @@ function prismaSchemaToSummaries(source, options = {}) {
|
|
|
95
113
|
relation.joinTable
|
|
96
114
|
);
|
|
97
115
|
}
|
|
98
|
-
for (const node of
|
|
116
|
+
for (const { node, file } of nodes) {
|
|
99
117
|
const n = node;
|
|
100
118
|
if (n.type !== "model" && n.type !== "view") {
|
|
101
119
|
continue;
|
|
@@ -106,16 +124,25 @@ function prismaSchemaToSummaries(source, options = {}) {
|
|
|
106
124
|
model,
|
|
107
125
|
modelNames,
|
|
108
126
|
enumNames,
|
|
127
|
+
tableOfModel,
|
|
109
128
|
storageSystem,
|
|
110
129
|
scope,
|
|
111
|
-
|
|
130
|
+
origin,
|
|
131
|
+
sourceFile: file,
|
|
112
132
|
joinContainerByField
|
|
113
133
|
})
|
|
114
134
|
);
|
|
115
135
|
}
|
|
116
136
|
for (const relation of relations) {
|
|
117
137
|
summaries.push(
|
|
118
|
-
buildJoinTableSummary({
|
|
138
|
+
buildJoinTableSummary({
|
|
139
|
+
relation,
|
|
140
|
+
tableOfModel,
|
|
141
|
+
storageSystem,
|
|
142
|
+
scope,
|
|
143
|
+
origin,
|
|
144
|
+
sourceFile: fileOfModel.get(relation.leftModel) ?? "schema.prisma"
|
|
145
|
+
})
|
|
119
146
|
);
|
|
120
147
|
}
|
|
121
148
|
return summaries;
|
|
@@ -198,6 +225,14 @@ function buildJoinTableSummary(opts) {
|
|
|
198
225
|
opts.relation.leftModel,
|
|
199
226
|
opts.relation.rightModel
|
|
200
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
|
+
});
|
|
201
236
|
return {
|
|
202
237
|
kind: "library",
|
|
203
238
|
location: {
|
|
@@ -222,14 +257,12 @@ function buildJoinTableSummary(opts) {
|
|
|
222
257
|
metadata: {
|
|
223
258
|
storageContract: {
|
|
224
259
|
fieldSet: "exhaustive",
|
|
225
|
-
fields: [
|
|
226
|
-
{ name: "A", type: modelA, nullable: false, primary: true },
|
|
227
|
-
{ name: "B", type: modelB, nullable: false, primary: true }
|
|
228
|
-
],
|
|
260
|
+
fields: [keyColumn("A", modelA), keyColumn("B", modelB)],
|
|
229
261
|
indexes: [
|
|
230
262
|
{ fields: ["A", "B"], unique: true },
|
|
231
263
|
{ fields: ["B"], unique: false }
|
|
232
|
-
]
|
|
264
|
+
],
|
|
265
|
+
...originOf(opts.origin)
|
|
233
266
|
}
|
|
234
267
|
}
|
|
235
268
|
};
|
|
@@ -238,21 +271,94 @@ function prismaSchemaFileToSummaries(schemaPath, options = {}) {
|
|
|
238
271
|
if (!import_node_fs.default.existsSync(schemaPath)) {
|
|
239
272
|
throw new Error(`Prisma schema not found: ${schemaPath}`);
|
|
240
273
|
}
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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";
|
|
246
346
|
}
|
|
247
347
|
function buildModelSummary(opts) {
|
|
248
348
|
const columns = [];
|
|
249
349
|
const indexes = [];
|
|
250
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
|
+
};
|
|
251
357
|
let hasRelation = false;
|
|
252
358
|
for (const property of opts.model.properties) {
|
|
253
359
|
if (property.type === "field") {
|
|
254
360
|
const field = property;
|
|
255
|
-
const column = fieldToColumn(field, opts.modelNames,
|
|
361
|
+
const column = fieldToColumn(field, opts.modelNames, context);
|
|
256
362
|
if (column !== null) {
|
|
257
363
|
columns.push(column);
|
|
258
364
|
}
|
|
@@ -306,7 +412,8 @@ function buildModelSummary(opts) {
|
|
|
306
412
|
fieldSet: "exhaustive",
|
|
307
413
|
fields: columns,
|
|
308
414
|
indexes,
|
|
309
|
-
...physicalTable !== null && physicalTable !== opts.model.name ? { physicalTable } : {}
|
|
415
|
+
...physicalTable !== null && physicalTable !== opts.model.name ? { physicalTable } : {},
|
|
416
|
+
...originOf(opts.origin)
|
|
310
417
|
}
|
|
311
418
|
}
|
|
312
419
|
};
|
|
@@ -336,37 +443,110 @@ function relationNameOf(field) {
|
|
|
336
443
|
const relation = relationAttributeOf(field);
|
|
337
444
|
return relation === void 0 ? null : readStringArg(relation);
|
|
338
445
|
}
|
|
339
|
-
function fieldToColumn(field, modelNames,
|
|
340
|
-
if (field.array === true) {
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
446
|
+
function fieldToColumn(field, modelNames, context) {
|
|
343
447
|
const ft = field.fieldType;
|
|
344
|
-
|
|
345
|
-
const isEnum = enumNames.has(ft);
|
|
346
|
-
const isRelation = modelNames.has(ft);
|
|
347
|
-
if (isRelation) {
|
|
448
|
+
if (modelNames.has(ft)) {
|
|
348
449
|
return null;
|
|
349
450
|
}
|
|
350
|
-
if (!
|
|
451
|
+
if (!PRISMA_SCALARS.has(ft) && !context.enumNames.has(ft)) {
|
|
351
452
|
return null;
|
|
352
453
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
} else if (attr.name === "unique") {
|
|
359
|
-
unique = true;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
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);
|
|
362
459
|
return {
|
|
363
460
|
name: field.name,
|
|
364
|
-
|
|
365
|
-
|
|
461
|
+
...column !== null && column !== field.name ? { column } : {},
|
|
462
|
+
type: columnTypeOf(field, context.enumNames),
|
|
463
|
+
...nullable === void 0 ? {} : { nullable },
|
|
366
464
|
...primary ? { primary: true } : {},
|
|
367
|
-
...unique ? { unique: true } : {}
|
|
465
|
+
...fieldAttribute(field, "unique") !== void 0 ? { unique: true } : {},
|
|
466
|
+
...databaseDefault === void 0 ? {} : { default: databaseDefault },
|
|
467
|
+
...foreignKey === void 0 ? {} : { foreignKey }
|
|
368
468
|
};
|
|
369
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
|
+
}
|
|
370
550
|
function physicalTableOf(model) {
|
|
371
551
|
for (const property of model.properties) {
|
|
372
552
|
if (property.type !== "attribute") {
|
|
@@ -376,13 +556,29 @@ function physicalTableOf(model) {
|
|
|
376
556
|
if (attr.name !== "map") {
|
|
377
557
|
continue;
|
|
378
558
|
}
|
|
379
|
-
const name =
|
|
559
|
+
const name = stringArgOf(attr);
|
|
380
560
|
if (name !== null) {
|
|
381
561
|
return name;
|
|
382
562
|
}
|
|
383
563
|
}
|
|
384
564
|
return null;
|
|
385
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
|
+
}
|
|
386
582
|
function readStringArg(attr) {
|
|
387
583
|
for (const arg of attr.args ?? []) {
|
|
388
584
|
const value = arg.value;
|
|
@@ -396,12 +592,15 @@ function blockAttributeToIndex(attr) {
|
|
|
396
592
|
if (attr.name !== "index" && attr.name !== "unique" && attr.name !== "id") {
|
|
397
593
|
return null;
|
|
398
594
|
}
|
|
399
|
-
const fields =
|
|
595
|
+
const fields = blockFieldsOf(attr);
|
|
400
596
|
if (fields === null) {
|
|
401
597
|
return null;
|
|
402
598
|
}
|
|
403
599
|
return { fields, unique: attr.name !== "index" };
|
|
404
600
|
}
|
|
601
|
+
function blockFieldsOf(attr) {
|
|
602
|
+
return readArrayArg(attr) ?? readKeyedArrayArg(attr, "fields");
|
|
603
|
+
}
|
|
405
604
|
function readArrayArg(attr) {
|
|
406
605
|
for (const arg of attr.args ?? []) {
|
|
407
606
|
const names = arrayNames(arg.value);
|
|
@@ -469,6 +668,7 @@ function readProviderString(ds) {
|
|
|
469
668
|
// Annotate the CommonJS export names for ESM import in node:
|
|
470
669
|
0 && (module.exports = {
|
|
471
670
|
prismaSchemaFileToSummaries,
|
|
671
|
+
prismaSchemaFiles,
|
|
472
672
|
prismaSchemaToSummaries,
|
|
473
673
|
prismaStorageSystem
|
|
474
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,10 +31,20 @@ 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.
|
|
30
46
|
*/
|
|
31
|
-
declare function
|
|
47
|
+
declare function prismaSchemaFiles(schemaPath: string): string[];
|
|
32
48
|
type PrismaStorageSystem = "postgresql" | "mysql" | "sqlite";
|
|
33
49
|
/**
|
|
34
50
|
* The storage system the datasource provider says, or null for MongoDB or
|
|
@@ -36,4 +52,4 @@ type PrismaStorageSystem = "postgresql" | "mysql" | "sqlite";
|
|
|
36
52
|
*/
|
|
37
53
|
declare function prismaStorageSystem(source: string): PrismaStorageSystem | null;
|
|
38
54
|
|
|
39
|
-
export { type PrismaSchemaToSummariesOptions, type PrismaStorageSystem, prismaSchemaFileToSummaries, prismaSchemaToSummaries, prismaStorageSystem };
|
|
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,10 +31,20 @@ 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.
|
|
30
46
|
*/
|
|
31
|
-
declare function
|
|
47
|
+
declare function prismaSchemaFiles(schemaPath: string): string[];
|
|
32
48
|
type PrismaStorageSystem = "postgresql" | "mysql" | "sqlite";
|
|
33
49
|
/**
|
|
34
50
|
* The storage system the datasource provider says, or null for MongoDB or
|
|
@@ -36,4 +52,4 @@ type PrismaStorageSystem = "postgresql" | "mysql" | "sqlite";
|
|
|
36
52
|
*/
|
|
37
53
|
declare function prismaStorageSystem(source: string): PrismaStorageSystem | null;
|
|
38
54
|
|
|
39
|
-
export { type PrismaSchemaToSummariesOptions, type PrismaStorageSystem, prismaSchemaFileToSummaries, prismaSchemaToSummaries, prismaStorageSystem };
|
|
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,8 +22,20 @@ 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);
|
|
@@ -41,13 +54,17 @@ function prismaSchemaToSummaries(source, options = {}) {
|
|
|
41
54
|
if (storageSystem === null) {
|
|
42
55
|
return [];
|
|
43
56
|
}
|
|
44
|
-
const sourceFile = options.source ?? "schema.prisma";
|
|
45
|
-
const scope = options.scope ?? "default";
|
|
46
57
|
const summaries = [];
|
|
47
58
|
const models = list.filter(
|
|
48
59
|
(node) => node.type === "model"
|
|
49
60
|
);
|
|
61
|
+
const fileOfModel = new Map(
|
|
62
|
+
nodes.map(({ node, file }) => [node.name, file])
|
|
63
|
+
);
|
|
50
64
|
const relations = implicitManyToManyRelations(models, modelNames);
|
|
65
|
+
const tableOfModel = new Map(
|
|
66
|
+
models.map((model) => [model.name, physicalTableOf(model) ?? model.name])
|
|
67
|
+
);
|
|
51
68
|
const joinContainerByField = /* @__PURE__ */ new Map();
|
|
52
69
|
for (const relation of relations) {
|
|
53
70
|
joinContainerByField.set(
|
|
@@ -59,7 +76,7 @@ function prismaSchemaToSummaries(source, options = {}) {
|
|
|
59
76
|
relation.joinTable
|
|
60
77
|
);
|
|
61
78
|
}
|
|
62
|
-
for (const node of
|
|
79
|
+
for (const { node, file } of nodes) {
|
|
63
80
|
const n = node;
|
|
64
81
|
if (n.type !== "model" && n.type !== "view") {
|
|
65
82
|
continue;
|
|
@@ -70,16 +87,25 @@ function prismaSchemaToSummaries(source, options = {}) {
|
|
|
70
87
|
model,
|
|
71
88
|
modelNames,
|
|
72
89
|
enumNames,
|
|
90
|
+
tableOfModel,
|
|
73
91
|
storageSystem,
|
|
74
92
|
scope,
|
|
75
|
-
|
|
93
|
+
origin,
|
|
94
|
+
sourceFile: file,
|
|
76
95
|
joinContainerByField
|
|
77
96
|
})
|
|
78
97
|
);
|
|
79
98
|
}
|
|
80
99
|
for (const relation of relations) {
|
|
81
100
|
summaries.push(
|
|
82
|
-
buildJoinTableSummary({
|
|
101
|
+
buildJoinTableSummary({
|
|
102
|
+
relation,
|
|
103
|
+
tableOfModel,
|
|
104
|
+
storageSystem,
|
|
105
|
+
scope,
|
|
106
|
+
origin,
|
|
107
|
+
sourceFile: fileOfModel.get(relation.leftModel) ?? "schema.prisma"
|
|
108
|
+
})
|
|
83
109
|
);
|
|
84
110
|
}
|
|
85
111
|
return summaries;
|
|
@@ -162,6 +188,14 @@ function buildJoinTableSummary(opts) {
|
|
|
162
188
|
opts.relation.leftModel,
|
|
163
189
|
opts.relation.rightModel
|
|
164
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
|
+
});
|
|
165
199
|
return {
|
|
166
200
|
kind: "library",
|
|
167
201
|
location: {
|
|
@@ -186,14 +220,12 @@ function buildJoinTableSummary(opts) {
|
|
|
186
220
|
metadata: {
|
|
187
221
|
storageContract: {
|
|
188
222
|
fieldSet: "exhaustive",
|
|
189
|
-
fields: [
|
|
190
|
-
{ name: "A", type: modelA, nullable: false, primary: true },
|
|
191
|
-
{ name: "B", type: modelB, nullable: false, primary: true }
|
|
192
|
-
],
|
|
223
|
+
fields: [keyColumn("A", modelA), keyColumn("B", modelB)],
|
|
193
224
|
indexes: [
|
|
194
225
|
{ fields: ["A", "B"], unique: true },
|
|
195
226
|
{ fields: ["B"], unique: false }
|
|
196
|
-
]
|
|
227
|
+
],
|
|
228
|
+
...originOf(opts.origin)
|
|
197
229
|
}
|
|
198
230
|
}
|
|
199
231
|
};
|
|
@@ -202,21 +234,94 @@ function prismaSchemaFileToSummaries(schemaPath, options = {}) {
|
|
|
202
234
|
if (!fs.existsSync(schemaPath)) {
|
|
203
235
|
throw new Error(`Prisma schema not found: ${schemaPath}`);
|
|
204
236
|
}
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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";
|
|
210
309
|
}
|
|
211
310
|
function buildModelSummary(opts) {
|
|
212
311
|
const columns = [];
|
|
213
312
|
const indexes = [];
|
|
214
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
|
+
};
|
|
215
320
|
let hasRelation = false;
|
|
216
321
|
for (const property of opts.model.properties) {
|
|
217
322
|
if (property.type === "field") {
|
|
218
323
|
const field = property;
|
|
219
|
-
const column = fieldToColumn(field, opts.modelNames,
|
|
324
|
+
const column = fieldToColumn(field, opts.modelNames, context);
|
|
220
325
|
if (column !== null) {
|
|
221
326
|
columns.push(column);
|
|
222
327
|
}
|
|
@@ -270,7 +375,8 @@ function buildModelSummary(opts) {
|
|
|
270
375
|
fieldSet: "exhaustive",
|
|
271
376
|
fields: columns,
|
|
272
377
|
indexes,
|
|
273
|
-
...physicalTable !== null && physicalTable !== opts.model.name ? { physicalTable } : {}
|
|
378
|
+
...physicalTable !== null && physicalTable !== opts.model.name ? { physicalTable } : {},
|
|
379
|
+
...originOf(opts.origin)
|
|
274
380
|
}
|
|
275
381
|
}
|
|
276
382
|
};
|
|
@@ -300,37 +406,110 @@ function relationNameOf(field) {
|
|
|
300
406
|
const relation = relationAttributeOf(field);
|
|
301
407
|
return relation === void 0 ? null : readStringArg(relation);
|
|
302
408
|
}
|
|
303
|
-
function fieldToColumn(field, modelNames,
|
|
304
|
-
if (field.array === true) {
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
409
|
+
function fieldToColumn(field, modelNames, context) {
|
|
307
410
|
const ft = field.fieldType;
|
|
308
|
-
|
|
309
|
-
const isEnum = enumNames.has(ft);
|
|
310
|
-
const isRelation = modelNames.has(ft);
|
|
311
|
-
if (isRelation) {
|
|
411
|
+
if (modelNames.has(ft)) {
|
|
312
412
|
return null;
|
|
313
413
|
}
|
|
314
|
-
if (!
|
|
414
|
+
if (!PRISMA_SCALARS.has(ft) && !context.enumNames.has(ft)) {
|
|
315
415
|
return null;
|
|
316
416
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
} else if (attr.name === "unique") {
|
|
323
|
-
unique = true;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
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);
|
|
326
422
|
return {
|
|
327
423
|
name: field.name,
|
|
328
|
-
|
|
329
|
-
|
|
424
|
+
...column !== null && column !== field.name ? { column } : {},
|
|
425
|
+
type: columnTypeOf(field, context.enumNames),
|
|
426
|
+
...nullable === void 0 ? {} : { nullable },
|
|
330
427
|
...primary ? { primary: true } : {},
|
|
331
|
-
...unique ? { unique: true } : {}
|
|
428
|
+
...fieldAttribute(field, "unique") !== void 0 ? { unique: true } : {},
|
|
429
|
+
...databaseDefault === void 0 ? {} : { default: databaseDefault },
|
|
430
|
+
...foreignKey === void 0 ? {} : { foreignKey }
|
|
332
431
|
};
|
|
333
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
|
+
}
|
|
334
513
|
function physicalTableOf(model) {
|
|
335
514
|
for (const property of model.properties) {
|
|
336
515
|
if (property.type !== "attribute") {
|
|
@@ -340,13 +519,29 @@ function physicalTableOf(model) {
|
|
|
340
519
|
if (attr.name !== "map") {
|
|
341
520
|
continue;
|
|
342
521
|
}
|
|
343
|
-
const name =
|
|
522
|
+
const name = stringArgOf(attr);
|
|
344
523
|
if (name !== null) {
|
|
345
524
|
return name;
|
|
346
525
|
}
|
|
347
526
|
}
|
|
348
527
|
return null;
|
|
349
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
|
+
}
|
|
350
545
|
function readStringArg(attr) {
|
|
351
546
|
for (const arg of attr.args ?? []) {
|
|
352
547
|
const value = arg.value;
|
|
@@ -360,12 +555,15 @@ function blockAttributeToIndex(attr) {
|
|
|
360
555
|
if (attr.name !== "index" && attr.name !== "unique" && attr.name !== "id") {
|
|
361
556
|
return null;
|
|
362
557
|
}
|
|
363
|
-
const fields =
|
|
558
|
+
const fields = blockFieldsOf(attr);
|
|
364
559
|
if (fields === null) {
|
|
365
560
|
return null;
|
|
366
561
|
}
|
|
367
562
|
return { fields, unique: attr.name !== "index" };
|
|
368
563
|
}
|
|
564
|
+
function blockFieldsOf(attr) {
|
|
565
|
+
return readArrayArg(attr) ?? readKeyedArrayArg(attr, "fields");
|
|
566
|
+
}
|
|
369
567
|
function readArrayArg(attr) {
|
|
370
568
|
for (const arg of attr.args ?? []) {
|
|
371
569
|
const names = arrayNames(arg.value);
|
|
@@ -432,6 +630,7 @@ function readProviderString(ds) {
|
|
|
432
630
|
}
|
|
433
631
|
export {
|
|
434
632
|
prismaSchemaFileToSummaries,
|
|
633
|
+
prismaSchemaFiles,
|
|
435
634
|
prismaSchemaToSummaries,
|
|
436
635
|
prismaStorageSystem
|
|
437
636
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/contract-prisma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Turn a Prisma schema into a suss contract and check it against the queries your code runs on those tables.",
|
|
6
6
|
"keywords": [
|
|
@@ -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.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"tsup": "*",
|