@aventara/prisma7-adapter 1.0.0-rc.2 → 1.0.0-rc.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,131 @@
1
1
  # @aventara/prisma7-adapter
2
2
 
3
+ ## 1.0.0-rc.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 7c3b65f: Build-time Contracts for serverless and edge cold starts.
8
+
9
+ - New package `@aventara/build`, bin `aventara-build`, a development dependency. It runs `src/aventara.config.ts` and
10
+ `src/aventara.adapter.ts` without a database client, compiles the Contracts with the project's `@aventara/core` and
11
+ writes `src/generated/aventara/contracts.generated.ts` (`aventaraContracts`), which a serverless or edge entry passes
12
+ to `createFramework` as `prebuilt`. A configuration that startup would refuse fails the build with the same
13
+ `CONFIG_…`/`COMPILER_…` diagnostics. `aventara-build --check` writes nothing and exits 1 when the file is missing or
14
+ stale, naming the Resources that changed.
15
+ - `@aventara/prisma7-adapter`: `describePrismaAdapter({ discovery, provider, driver })` states the adapter's passive
16
+ facts (the model, the driver's transaction capability, the referential actions, the computed fields) with no client.
17
+ `createPrismaAdapter({ ...description, client })` builds the adapter from the same facts, and still checks the client
18
+ against the discovery artifact.
19
+ - `@aventara/cli`: the scaffold's `src/aventara.config.ts` exports the configuration as a plain value
20
+ (`export const aventaraConfig = { … } as const`) with no `adapter`. A new `src/aventara.adapter.ts` default-exports
21
+ `describePrismaAdapter(…)`, and `app.module.ts` joins the two:
22
+ `config: { ...aventaraConfig, adapter: await createPrismaAdapter({ ...adapter, client: prisma }) }`. The Nest server
23
+ still compiles its Contracts at startup and passes no `prebuilt`. `aventara:prepare` ends with `&& aventara-build`,
24
+ and `@aventara/build` is an exact development dependency.
25
+
26
+ - 2b1b17e: `aventara-build`'s files no longer land inside a Prisma client generated into `src/generated`.
27
+
28
+ When `aventara init` reuses a project whose Prisma client is generated into `src/generated`, it already puts the
29
+ discovery artifact in `src/aventara/`, because `prisma generate` deletes every file in its output directory that it did
30
+ not write. `aventara-build` still wrote its three files to `src/generated/aventara/`, so each `prisma generate` deleted
31
+ them and the project stopped compiling until the next `aventara:prepare`. Now all of Aventara's generated files are
32
+ in one place.
33
+
34
+ - `@aventara/build`: `aventara-build` writes `resources.generated.ts`, `computed-fields.generated.ts` and
35
+ `contracts.generated.ts` to the directory the configuration imports `aventaraResources` from, so the tree is always
36
+ written where the configuration reads it. A configuration that imports no tree keeps `src/generated/aventara/`.
37
+ The files of a project whose client is generated anywhere else are the same bytes, in the same place.
38
+ - `@aventara/prisma7-adapter`: with `--config`, the artifact imports the computed fields from its sibling,
39
+ `./computed-fields.generated`, wherever `--out` puts it. `--config` is still required for that import and the step
40
+ still checks that the file exists, but the step no longer uses its location. A scaffold's artifact is unchanged.
41
+ - `@aventara/cli`: in that project, `src/aventara.config.ts` imports the tree from `./aventara/resources.generated`,
42
+ and `.gitignore` gets the one `/src/aventara/` line that covers all four files (no separate
43
+ `/src/generated/aventara/contracts.generated.ts` line). The agent skill says where the generated files are in
44
+ both cases.
45
+
46
+ - 3a0dfca: A write the policy checks costs fewer database round trips. What each check decides and refuses is unchanged.
47
+
48
+ - A single-row update or delete carries the caller's scopes inside its own `where` and runs as one statement, with no
49
+ read of its row before it. One that matches nothing still answers `A4002` when the caller can read the row and
50
+ `A2003` when it cannot, with one lookup on that miss only. The Prisma adapter sends it as
51
+ `update({ where: { id, AND: [scope] } })`.
52
+ - A create asks `can({ action: "create", row })` and its `exists` lookups before the insert, without a transaction. A
53
+ transaction around them never closed the race between the lookup and the insert at the database's default
54
+ isolation, and cost a BEGIN and a COMMIT.
55
+ - A transaction opens only when a check after the write may roll it back: an update's after check or move-out, a
56
+ created row held to a write grant, a nested write, or returned rows checked by `exists`. A plain checked create or
57
+ delete is one statement, its interceptors and hooks outside any transaction.
58
+ - The after check asks `can` about the row the update returned, so it reads nothing again. Only a move-out reads the
59
+ changed rows again. An `update.count` runs as the returning `update.many` when the Contract offers it.
60
+
61
+ Round trips per request on PostgreSQL: `create.one` 3 → 1, `delete.unique` 4 → 1, `update.unique` 5 → 3,
62
+ `update.many` 4 → 3, `update.count` 5 → 3. Reads, and a Framework without a policy, are unchanged.
63
+
64
+ - 3813207: The Resource tree and the computed fields come from `aventara-build`.
65
+
66
+ - `@aventara/build`: before it compiles the Contracts, `aventara-build` writes two more files under
67
+ `src/generated/aventara/`. `resources.generated.ts` exports `aventaraResources`: every `defineResource(…)` result
68
+ that the files `src/resources/**/*.resource.ts` export, found by its type (never run), one `resources` tree per
69
+ layer, written as the plain spreads you wrote by hand (`root` always, `application`, `client` and `agent` when some
70
+ Resource has a block for them). Two definitions of one Resource stop the build, naming both. Imports are spelled as
71
+ the project spells its own. `computed-fields.generated.ts` exports `computedFields` and its type `AvComputedFields`:
72
+ which fields the configuration's write compute fills, read off its type after the tree exists, so a fresh install is
73
+ typed right on its first `aventara:prepare`. `--check` covers all three files. `typescript` (`>=5.7.0 <7`) is now a
74
+ peer dependency.
75
+ - `@aventara/prisma7-adapter`: the generate step no longer reads the configuration. With `--config`, the discovery
76
+ artifact imports `computedFields` and `AvComputedFields` from the file `aventara-build` writes beside the
77
+ configuration (`generated/aventara/computed-fields.generated.ts`); a `--config` file that does not exist is still
78
+ refused. `readConfigComputedFields`, `AvPrismaConfigReaderError` and `ConfigComputedFieldsReading` are removed from
79
+ `@aventara/prisma7-adapter/generate`, as are `PrismaGenerateToolchain.readComputedFields` and
80
+ `PrismaGenerateResult.computedFields`.
81
+ - `@aventara/cli`: the scaffold's `src/aventara.config.ts` takes its Resources from the tree,
82
+ `resources: aventaraResources.root`, so a new Resource file needs no configuration edit; the agent skill says so.
83
+
84
+ - Updated dependencies [e3d03a7]
85
+ - Updated dependencies [d10639a]
86
+ - Updated dependencies [f67e68c]
87
+ - Updated dependencies [708556c]
88
+ - Updated dependencies [3a0dfca]
89
+ - Updated dependencies [99169d2]
90
+ - Updated dependencies [0fd2608]
91
+ - Updated dependencies [154e440]
92
+ - Updated dependencies [2fcffdb]
93
+ - @aventara/core@1.0.0-rc.4
94
+
95
+ ## 1.0.0-rc.3
96
+
97
+ ### Patch Changes
98
+
99
+ - b38034d: `aventara-prisma7-generate` refuses a `--provider` that the schema's `datasource` block contradicts, or a block naming
100
+ a provider the adapter does not support, before `prisma generate` runs, so neither the Prisma client nor the artifact
101
+ is written:
102
+
103
+ ```text
104
+ aventara-prisma7-generate: --provider postgresql contradicts the schema's datasource `db` (prisma/schema.prisma:12), which names provider "sqlite"; pass --provider sqlite.
105
+ ```
106
+
107
+ PSL's `postgres` spelling counts as `postgresql`, and a provider that is not a string literal is not compared.
108
+ Before, the flag was taken on trust, and a wrong one wrote the wrong provider into the artifact and the wrong filter
109
+ modes into the Contract.
110
+
111
+ When the file system refuses the artifact file (`--out`), the step says so in one sentence naming the file and why,
112
+ such as
113
+ `the artifact file /app/src/generated/aventara/discovery.artifact.ts (--out) cannot be written: permission denied (EACCES).`,
114
+ and leaves no staged file behind. Before, every such failure except a directory printed a stack trace.
115
+
116
+ - b38034d: On PostgreSQL, a `@unique` value too long for its index (an index row past the B-tree's limit of about 2704 bytes, SQLSTATE
117
+ `54000`) is answered `422 A2004`, not `500 A3001`: one `V1004` issue at `["arguments"]`, as a value too long for its
118
+ column already was. The adapter reads the SQLSTATE, never the message. The issue's message for a value too long now
119
+ reads `The database refused this value: it is longer than the database can hold for its column.`
120
+ (it said `longer than its column can hold`, which an index refusal is not).
121
+ - Updated dependencies [b38034d]
122
+ - Updated dependencies [b38034d]
123
+ - Updated dependencies [b38034d]
124
+ - Updated dependencies [b38034d]
125
+ - Updated dependencies [b38034d]
126
+ - Updated dependencies [b38034d]
127
+ - @aventara/core@1.0.0-rc.3
128
+
3
129
  ## 1.0.0-rc.2
4
130
 
5
131
  ### Patch Changes
package/README.md CHANGED
@@ -55,6 +55,31 @@ than a declaration that is checked.
55
55
  `createPrismaAdapter` is `async`. Construction verifies the discovery artifact, and the digest it
56
56
  checks is a `crypto.subtle` promise, so every check runs once, here, before an adapter exists.
57
57
 
58
+ ### `describePrismaAdapter`: the same facts, with no client
59
+
60
+ `describePrismaAdapter({ discovery, provider, driver })` returns the adapter's passive facts: the model,
61
+ the driver's transaction capability, the referential actions in Aventara's words and the computed
62
+ fields the generated types were written with. It constructs nothing and connects to nothing, so
63
+ `aventara-build` (`@aventara/build`) compiles the Contracts from it at build time. Spread into
64
+ `createPrismaAdapter` with the client, it is the adapter's options, and the adapter states exactly the
65
+ facts the description does:
66
+
67
+ ```ts
68
+ // src/aventara.adapter.ts
69
+ export default describePrismaAdapter({
70
+ discovery,
71
+ provider: "postgresql",
72
+ driver: "@prisma/adapter-pg",
73
+ });
74
+
75
+ // where the framework is created
76
+ import description from "./aventara.adapter.js";
77
+ const adapter = await createPrismaAdapter({ ...description, client });
78
+ ```
79
+
80
+ `createPrismaAdapter` still verifies the artifact and checks the client against it; the description
81
+ does neither, because it has no client to check.
82
+
58
83
  ### `maxWait`, `timeout` and `isolationLevel` are adapter-owned
59
84
 
60
85
  They are provider concepts. Core's `Adapter.transaction(work)` interface carries no options and will
@@ -86,8 +111,9 @@ npx aventara-prisma7-generate \
86
111
  --config src/aventara.config.ts
87
112
  ```
88
113
 
89
- `--provider` and `--driver` are declared, never sniffed. The provider is recorded in the artifact. The
90
- driver decides `AvAdapterModel.transactions`, a driver fact the schema cannot state. Declare the driver
114
+ `--provider` and `--driver` are declared, never sniffed. The provider is recorded in the artifact, and the
115
+ step refuses a `--provider` that the schema's `datasource` block contradicts (`provider = "sqlite"` with
116
+ `--provider postgresql`), naming the flag to pass. The driver decides `AvAdapterModel.transactions`, a driver fact the schema cannot state. Declare the driver
91
117
  the application will actually construct the adapter with. The two cannot drift silently, but they can
92
118
  disagree, and core refuses construction when they do.
93
119
 
@@ -98,16 +124,18 @@ installed keeps `@prisma/prisma7` beside it. With neither usable, the step refus
98
124
  both. TypeScript 6 is supported; TypeScript 7, which has no classic compiler API, is refused in one
99
125
  sentence.
100
126
 
101
- `--config` is optional. It names your Aventara configuration module: its `aventaraConfig` export (a
102
- function, as the `aventara` scaffold writes it, or a value) or its default export. The step reads the
103
- configuration's TypeScript type, never runs it, and states in the artifact which fields its
104
- `compute.create` and `compute.update` stages fill, per layer (`computedFields`). A variant's guards,
105
- pipes and filters, which run before write compute, may then leave exactly those fields out of a create;
106
- every other required field stays required. Without `--config`, or when the configuration is not a
107
- literal where a compute stage is written (the step prints where), the artifact states nothing and those
108
- stages may leave any create field out. When the framework starts, it compares the statement with the
109
- configuration and warns (`AVENTARA_STALE_GENERATED_TYPES`) if you changed a compute stage without
110
- running the step again.
127
+ `--config` is optional. It names your Aventara configuration module (`src/aventara.config.ts`). The step
128
+ neither reads nor runs it: `aventara-build` (`@aventara/build`), which `aventara:prepare` runs right after
129
+ this step, reads the configuration's TypeScript type and writes which fields its `compute.create` and
130
+ `compute.update` stages fill, per layer, to `computed-fields.generated.ts` beside the artifact (the
131
+ directory the configuration imports its Resource tree from). The artifact imports that sibling file: `computedFields` (also `discovery.computedFields`) and its type, which the
132
+ model's type carries. A variant's guards, pipes and filters, which run before write compute, may then
133
+ leave exactly those fields out of a create; every other required field stays required. On a fresh install
134
+ the file is not there until `aventara-build` has run. Without `--config`, or when the configuration is not
135
+ a literal where a compute stage is written (`aventara-build` prints where), the artifact states nothing and
136
+ those stages may leave any create field out. When the framework starts, it compares the statement with
137
+ the configuration and warns (`AVENTARA_STALE_GENERATED_TYPES`) if you changed a compute stage without
138
+ running `aventara:prepare` again.
111
139
 
112
140
  `--out` must not be inside `--client`: Prisma's next `prisma generate` clears that directory and would
113
141
  delete the artifact. The step refuses such an `--out`, and one that names a directory, in one sentence
@@ -107,11 +107,13 @@ export type AvPrismaDiscoveryArtifact<M extends AvPrismaAdapterModel = AvPrismaA
107
107
  */
108
108
  readonly client: PrismaClientShape;
109
109
  /**
110
- * Which fields the configuration's write compute fills, per layer, as the
111
- * generate step read them off the configuration it was given (`--config`);
112
- * `null` when it was given none. The model's type carries the same statement
113
- * for the pipeline stages' types; this value is compared with the
114
- * configuration when the framework starts. Not part of `modelHash`.
110
+ * Which fields the configuration's write compute fills, per layer, as
111
+ * `aventara-build` read them off the configuration's type (the artifact
112
+ * imports them when the step was given `--config`); `null` when it was
113
+ * given none, or the build could not read them. The model's type carries
114
+ * the same statement for the pipeline stages' types; this value is compared
115
+ * with the configuration when the framework starts. Not part of
116
+ * `modelHash`.
115
117
  */
116
118
  readonly computedFields: AvConfigComputedFields | null;
117
119
  };
@@ -26,8 +26,16 @@ export type PrismaSchemaUnknownKind =
26
26
  | "block"
27
27
  /** A line the reader could not classify at all. */
28
28
  | "line";
29
+ /** A `datasource` block: the provider it names. */
30
+ export type PrismaSchemaDatasource = {
31
+ readonly name: string;
32
+ readonly provider: string | null;
33
+ readonly site: PrismaSchemaSite;
34
+ };
29
35
  export type PrismaSchemaFacts = {
30
36
  readonly enums: readonly PrismaSchemaEnum[];
37
+ /** Every `datasource` block, in reading order (Prisma admits one). */
38
+ readonly datasources: readonly PrismaSchemaDatasource[];
31
39
  readonly models: readonly PrismaSchemaModel[];
32
40
  /**
33
41
  * Attributes and blocks the reader does not recognise at all (a `type`/`view`
@@ -63,12 +63,15 @@ export function readPrismaSchemaSources(sources) {
63
63
  const enums = [];
64
64
  const models = [];
65
65
  const unhandled = [];
66
+ const datasources = [];
66
67
  for (const source of sources) {
67
- readOneSource(source, enums, models, unhandled);
68
+ readOneSource(source, enums, models, unhandled, datasources);
68
69
  }
69
- return { enums, models, unhandled };
70
+ return { enums, datasources, models, unhandled };
70
71
  }
71
- function readOneSource(source, enums, models, unhandled) {
72
+ const DATASOURCE_PROVIDER = /^provider\s*=\s*(.*)$/;
73
+ const STRING_LITERAL = /^"([^"\\]*)"$/;
74
+ function readOneSource(source, enums, models, unhandled, datasources) {
72
75
  let open = null;
73
76
  const lines = source.text.split("\n");
74
77
  const unreadableActions = new Set();
@@ -105,12 +108,26 @@ function readOneSource(source, enums, models, unhandled) {
105
108
  site: open.site,
106
109
  });
107
110
  }
111
+ else if (open.kind === "datasource") {
112
+ datasources.push({
113
+ name: open.name,
114
+ provider: open.provider,
115
+ site: open.site,
116
+ });
117
+ }
108
118
  open = null;
109
119
  continue;
110
120
  }
111
121
  if (open.kind === "skip") {
112
122
  continue;
113
123
  }
124
+ if (open.kind === "datasource") {
125
+ const assigned = DATASOURCE_PROVIDER.exec(line)?.[1]?.trim();
126
+ if (assigned !== undefined) {
127
+ open.provider = STRING_LITERAL.exec(assigned)?.[1] ?? null;
128
+ }
129
+ continue;
130
+ }
114
131
  if (open.kind === "enum") {
115
132
  if (line.startsWith("@@")) {
116
133
  for (const attribute of splitAttributes(line)) {
@@ -175,6 +192,9 @@ function openBlock(line, site, record) {
175
192
  if (keyword === "enum") {
176
193
  return { kind: "enum", name, site, values: [] };
177
194
  }
195
+ if (keyword === "datasource") {
196
+ return { kind: "datasource", name, site, provider: null };
197
+ }
178
198
  if (keyword === "type" || keyword === "view") {
179
199
  record("block", keyword, line);
180
200
  }
@@ -29,6 +29,7 @@ export const PRISMA_DATABASE_VALUE_TABLE = {
29
29
  "22025": "trailingEscape",
30
30
  "22009": "datetimeOutOfRange",
31
31
  "22012": "divisionByZero",
32
+ "54000": "tooLong",
32
33
  };
33
34
  function refusedValue(facts) {
34
35
  if (facts.code === undefined) {
@@ -1,4 +1,3 @@
1
- import type { AvConfigComputedFields } from "@aventara/core";
2
1
  import { type AvPrismaAdapterModel, type AvPrismaReferentialActionTable, type PrismaClientShape, type PrismaTranslationTable } from "../discovery/discovery-artifact.interface.js";
3
2
  import type { AvPrismaProvider } from "../provider/provider-profile.js";
4
3
  export type PrismaArtifactEmission = {
@@ -12,9 +11,14 @@ export type PrismaArtifactEmission = {
12
11
  /** The generated client's models and field references — beside `model`, outside the hash. */
13
12
  readonly client: PrismaClientShape;
14
13
  /**
15
- * What the configuration's write compute fills, read off its type; `null`
16
- * when the step was given no configuration. Beside `model`, outside the hash.
14
+ * The specifier the module imports the configuration's computed fields by:
15
+ * the file `aventara-build` writes beside the configuration
16
+ * (`computed-fields.generated.ts`). `null` when the step was given no
17
+ * configuration: the module then states none. Beside `model`, outside the
18
+ * hash.
17
19
  */
18
- readonly computedFields: AvConfigComputedFields | null;
20
+ readonly computedFields: {
21
+ readonly specifier: string;
22
+ } | null;
19
23
  };
20
24
  export declare function emitDiscoveryArtifactModule(emission: PrismaArtifactEmission): Promise<string>;
@@ -60,8 +60,8 @@ const MODEL_TYPE_DOC = [
60
60
  const COMPUTED_FIELDS_DOC = [
61
61
  "/**",
62
62
  " * Which fields the configuration's write compute fills (`compute.create`,",
63
- " * `compute.update`), per layer and Resource, read off the configuration's type",
64
- " * by the generate step. A variant's guards, pipes and filters may leave a",
63
+ " * `compute.update`), per layer and Resource, as aventara-build read them off",
64
+ " * the configuration's type. A variant's guards, pipes and filters may leave a",
65
65
  " * create's computed fields out; every other required field stays required.",
66
66
  " * The framework warns at startup when the configuration says otherwise: run",
67
67
  " * `aventara:prepare` again. Not part of `modelHash`.",
@@ -90,10 +90,7 @@ function computedFieldsLines(computedFields) {
90
90
  }
91
91
  return [
92
92
  ...COMPUTED_FIELDS_DOC,
93
- `export const computedFields = ${canonicalLiteral(computedFields)} as const satisfies AvConfigComputedFields;`,
94
- "",
95
- "/** The type of {@link computedFields}: what `AvModel` carries to the stages' types. */",
96
- "export type AvComputedFields = typeof computedFields;",
93
+ "export { type AvComputedFields, computedFields };",
97
94
  "",
98
95
  ];
99
96
  }
@@ -102,9 +99,12 @@ export async function emitDiscoveryArtifactModule(emission) {
102
99
  return `${[
103
100
  ...BANNER,
104
101
  "",
105
- emission.computedFields === null
106
- ? 'import type { AvAdapterModel } from "@aventara/core";'
107
- : 'import type { AvAdapterModel, AvConfigComputedFields } from "@aventara/core";',
102
+ 'import type { AvAdapterModel } from "@aventara/core";',
103
+ ...(emission.computedFields === null
104
+ ? []
105
+ : [
106
+ `import { type AvComputedFields, computedFields } from ${JSON.stringify(emission.computedFields.specifier)};`,
107
+ ]),
108
108
  "",
109
109
  `const discoveredModel = ${canonicalLiteral(emission.model)} as const satisfies AvAdapterModel;`,
110
110
  "",
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The specifier the artifact at `artifactFile` imports the computed fields by:
3
+ * the sibling `computed-fields.generated` module.
4
+ */
5
+ export declare function computedFieldsSpecifier(artifactFile: string): string;
@@ -0,0 +1,52 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import ts from "typescript";
4
+ function projectOptions(directory) {
5
+ const tsconfig = ts.findConfigFile(directory, ts.sys.fileExists);
6
+ if (tsconfig === undefined) {
7
+ return {};
8
+ }
9
+ const read = ts.readConfigFile(tsconfig, ts.sys.readFile);
10
+ return read.error !== undefined
11
+ ? {}
12
+ : ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(tsconfig))
13
+ .options;
14
+ }
15
+ function packageType(directory) {
16
+ for (let current = path.resolve(directory);;) {
17
+ const manifest = path.join(current, "package.json");
18
+ if (existsSync(manifest)) {
19
+ try {
20
+ return JSON.parse(readFileSync(manifest, "utf8")).type;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ const parent = path.dirname(current);
27
+ if (parent === current) {
28
+ return undefined;
29
+ }
30
+ current = parent;
31
+ }
32
+ }
33
+ function importSuffix(directory) {
34
+ const options = projectOptions(directory);
35
+ if (options.allowImportingTsExtensions === true ||
36
+ options
37
+ .rewriteRelativeImportExtensions === true) {
38
+ return ".ts";
39
+ }
40
+ if (options.module === ts.ModuleKind.CommonJS ||
41
+ options.moduleResolution === ts.ModuleResolutionKind.Bundler) {
42
+ return "";
43
+ }
44
+ if (options.module === ts.ModuleKind.Node16 ||
45
+ options.module === ts.ModuleKind.NodeNext) {
46
+ return packageType(directory) === "module" ? ".js" : "";
47
+ }
48
+ return ".js";
49
+ }
50
+ export function computedFieldsSpecifier(artifactFile) {
51
+ return `./computed-fields.generated${importSuffix(path.dirname(path.resolve(artifactFile)))}`;
52
+ }
@@ -14,10 +14,13 @@ Options:
14
14
  --driver <name> ${PRISMA_DRIVERS.join(" | ")}
15
15
  --out <file> Where to write the emitted artifact module (a .ts file).
16
16
  --config <file> Optional: your Aventara configuration module, such as
17
- src/aventara.config.ts (its \`aventaraConfig\` export, or
18
- its default). The artifact states which fields it
19
- computes, so the stages that run before compute may leave
20
- exactly those out of a create.
17
+ src/aventara.config.ts. aventara-build reads which fields
18
+ it computes and writes them to
19
+ computed-fields.generated.ts beside the artifact; the
20
+ artifact imports them from there, so the stages that
21
+ run before compute may leave exactly those out of a
22
+ create. This step never reads the configuration; it
23
+ only checks that it exists.
21
24
  -h, --help Print this and exit 0.
22
25
 
23
26
  The artifact must NOT be written inside --client: the next \`prisma generate\`
@@ -94,12 +97,6 @@ async function main(argv) {
94
97
  for (const absence of result.excluded) {
95
98
  process.stdout.write(`excluded: ${absence.message}\n`);
96
99
  }
97
- if (result.computedFields?.kind === "read") {
98
- process.stdout.write(`computed fields: read from ${options.config ?? "the configuration"}\n`);
99
- }
100
- else if (result.computedFields?.kind === "unread") {
101
- process.stdout.write(`computed fields: not read, ${result.computedFields.reason}; the stages before compute may leave any create field out. Write the configuration \`as const\` there.\n`);
102
- }
103
100
  for (const exemption of result.exemptions) {
104
101
  process.stdout.write(`exempt (${exemption.kind}) ${exemption.entity}: ${exemption.reason}\n`);
105
102
  }
@@ -114,7 +111,6 @@ export async function runFromProcess() {
114
111
  (error instanceof Error &&
115
112
  (error.name === "AvPrismaGenerateStepError" ||
116
113
  error.name === "AvPrismaModelCompositionError" ||
117
- error.name === "AvPrismaConfigReaderError" ||
118
114
  error.name === "AvPrismaSchemaReaderError"));
119
115
  process.stderr.write(named
120
116
  ? `aventara-prisma7-generate: ${error.message}\n`
@@ -1,8 +1,6 @@
1
1
  import type { AvPrismaDriver, AvPrismaProvider } from "../provider/provider-profile.js";
2
2
  import type { PrismaGenerateResult, PrismaGenerateToolchain } from "./prisma.generate.js";
3
3
  export { AvPrismaModelCompositionError, createPrismaModelComposer, } from "../discovery/model.discoverer.js";
4
- export type { ConfigComputedFieldsReading } from "./computed-fields.reader.js";
5
- export { AvPrismaConfigReaderError, readConfigComputedFields, } from "./computed-fields.reader.js";
6
4
  export type { PrismaEntityAbsence } from "./existence.gate.js";
7
5
  export type { PrismaComposedArtifact, PrismaGenerateRequest, PrismaGenerateResult, PrismaGenerateToolchain, PrismaModelComposer, } from "./prisma.generate.js";
8
6
  export { AvPrismaGenerateStepError, createPrismaGenerateToolchain, PINNED_PRISMA_MAJOR, runPrismaGenerateStep, } from "./prisma.generate.js";
@@ -19,9 +17,9 @@ export type PrismaDiscoveryGenerateRequest = {
19
17
  /** Where the emitted TypeScript artifact module is written. */
20
18
  readonly artifactFile: string;
21
19
  /**
22
- * The Aventara configuration module whose computed fields the artifact
23
- * states (`src/aventara.config.ts` in a scaffolded project); left out, it
24
- * states none.
20
+ * The Aventara configuration module (`src/aventara.config.ts` in a
21
+ * scaffolded project): the artifact imports the fields it computes from
22
+ * the file `aventara-build` writes beside it. Left out, it states none.
25
23
  */
26
24
  readonly config?: string;
27
25
  /** Replaces the step's external effects; omit it to use the installed Prisma and TypeScript. */
@@ -2,7 +2,6 @@ import { createPrismaModelComposer } from "../discovery/model.discoverer.js";
2
2
  import { resolveProviderProfile } from "../provider/provider-profile.js";
3
3
  import { runPrismaGenerateStep } from "./prisma.generate.js";
4
4
  export { AvPrismaModelCompositionError, createPrismaModelComposer, } from "../discovery/model.discoverer.js";
5
- export { AvPrismaConfigReaderError, readConfigComputedFields, } from "./computed-fields.reader.js";
6
5
  export { AvPrismaGenerateStepError, createPrismaGenerateToolchain, PINNED_PRISMA_MAJOR, runPrismaGenerateStep, } from "./prisma.generate.js";
7
6
  export async function generatePrismaDiscoveryArtifact(request) {
8
7
  const profile = resolveProviderProfile({
@@ -2,7 +2,6 @@ import type { PrismaCapabilitySurface } from "../discovery/capability-surface.in
2
2
  import type { AvPrismaAdapterModel, AvPrismaReferentialActionTable, PrismaTranslationTable } from "../discovery/discovery-artifact.interface.js";
3
3
  import type { PrismaSchemaFacts } from "../discovery/schema-facts.interface.js";
4
4
  import type { AvPrismaProvider } from "../provider/provider-profile.js";
5
- import { type ConfigComputedFieldsReading } from "./computed-fields.reader.js";
6
5
  import type { PrismaEntityAbsence } from "./existence.gate.js";
7
6
  import { type PrismaCli } from "./prisma-cli.resolver.js";
8
7
  import type { PrismaReaderExemption } from "./reader-crosscheck.js";
@@ -38,11 +37,6 @@ export type PrismaGenerateToolchain = {
38
37
  readonly readSchema: (location: string) => Promise<PrismaSchemaFacts>;
39
38
  readonly readSurface: (clientDirectory: string) => PrismaCapabilitySurface;
40
39
  readonly writeArtifact: (file: string, source: string) => Promise<void>;
41
- /**
42
- * Reads which fields a configuration computes off its type. Optional: the
43
- * TypeScript reader is used when it is left out.
44
- */
45
- readonly readComputedFields?: (configFile: string) => ConfigComputedFieldsReading;
46
40
  };
47
41
  /** What {@link runPrismaGenerateStep} needs. */
48
42
  export type PrismaGenerateRequest = {
@@ -55,8 +49,11 @@ export type PrismaGenerateRequest = {
55
49
  readonly composeModel: PrismaModelComposer;
56
50
  /**
57
51
  * The application's Aventara configuration module (`src/aventara.config.ts`
58
- * in a scaffolded project), whose computed fields the artifact states. Left
59
- * out, the artifact states none (`computedFields: null`).
52
+ * in a scaffolded project). The artifact imports the fields it computes
53
+ * from the file `aventara-build` writes beside the artifact
54
+ * (`computed-fields.generated.ts`, a sibling); the step never reads the
55
+ * configuration, and only checks it exists. Left out, the artifact states
56
+ * none (`computedFields: null`).
60
57
  */
61
58
  readonly config?: string;
62
59
  readonly toolchain?: PrismaGenerateToolchain;
@@ -71,11 +68,6 @@ export type PrismaGenerateResult = {
71
68
  /** PSL-only entities the schema itself explained away. */
72
69
  readonly excluded: readonly PrismaEntityAbsence[];
73
70
  readonly exemptions: readonly PrismaReaderExemption[];
74
- /**
75
- * What the step made of the configuration's computed fields: read, or why
76
- * not. Absent when it was given no configuration.
77
- */
78
- readonly computedFields?: ConfigComputedFieldsReading;
79
71
  };
80
72
  export declare function resolveInstalledPrismaCli(): PrismaCli;
81
73
  /**
@@ -4,11 +4,10 @@ import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import ts from "typescript";
6
6
  import { computeModelHash } from "../discovery/artifact.loader.js";
7
- import { DISCOVERY_ARTIFACT_FORMAT } from "../discovery/discovery-artifact.interface.js";
8
- import { readPrismaSchema } from "../discovery/schema.reader.js";
7
+ import { AvPrismaSchemaReaderError, readPrismaSchema, } from "../discovery/schema.reader.js";
9
8
  import { readPrismaCapabilitySurface } from "../discovery/types.reader.js";
10
9
  import { emitDiscoveryArtifactModule } from "./artifact.emitter.js";
11
- import { readConfigComputedFields, } from "./computed-fields.reader.js";
10
+ import { computedFieldsSpecifier } from "./computed-fields.import.js";
12
11
  import { readDeclaredSupport } from "./declared-support.reader.js";
13
12
  import { applyExistenceGate } from "./existence.gate.js";
14
13
  import { resolvePrismaCli } from "./prisma-cli.resolver.js";
@@ -76,21 +75,38 @@ async function writeArtifactAtomically(file, source) {
76
75
  if (current === source) {
77
76
  return;
78
77
  }
79
- await mkdir(path.dirname(file), { recursive: true });
80
78
  const staged = `${file}.${process.pid}.${Date.now()}.tmp`;
81
79
  try {
80
+ await mkdir(path.dirname(file), { recursive: true });
82
81
  await writeFile(staged, source, "utf8");
83
82
  await rename(staged, file);
84
83
  }
85
84
  catch (error) {
86
- await rm(staged, { force: true });
87
- throw error;
85
+ await rm(staged, { force: true }).catch(() => undefined);
86
+ throw unwritableArtifact(file, error);
88
87
  }
89
88
  }
90
- async function describesModel(file, modelHash) {
91
- const current = await readFile(file, "utf8").catch(() => undefined);
92
- return (current?.includes(`\tregenerate_with_aventara_prepare: ${DISCOVERY_ARTIFACT_FORMAT},\n`) === true &&
93
- current.includes(`\tmodelHash: ${JSON.stringify(modelHash)},\n`));
89
+ const UNWRITABLE_BECAUSE = {
90
+ EACCES: "permission denied",
91
+ EPERM: "permission denied",
92
+ ENOTDIR: "a part of its path is a file, not a directory",
93
+ EEXIST: "a part of its path is a file, not a directory",
94
+ ENOSPC: "no space is left on the device",
95
+ EROFS: "the file system is read-only",
96
+ EISDIR: "it is a directory",
97
+ ENAMETOOLONG: "its path is too long",
98
+ };
99
+ function unwritableArtifact(file, error) {
100
+ const code = error instanceof Error && "code" in error && typeof error.code === "string"
101
+ ? error.code
102
+ : undefined;
103
+ if (code === undefined) {
104
+ return error;
105
+ }
106
+ const because = Object.hasOwn(UNWRITABLE_BECAUSE, code)
107
+ ? `${UNWRITABLE_BECAUSE[code]} (${code})`
108
+ : code;
109
+ return new AvPrismaGenerateStepError(`the artifact file ${path.resolve(file)} (--out) cannot be written: ${because}.`);
94
110
  }
95
111
  function majorOf(version) {
96
112
  const major = /^(\d+)\./.exec(version)?.[1];
@@ -126,6 +142,28 @@ async function assertArtifactPlacement(artifactFile, clientDirectory) {
126
142
  throw new AvPrismaGenerateStepError(`the artifact file ${file} (--out) is a directory; name the .ts file to write, such as ${path.join(file, "discovery.artifact.ts")}.`);
127
143
  }
128
144
  }
145
+ const PSL_PROVIDERS = {
146
+ sqlite: "sqlite",
147
+ postgresql: "postgresql",
148
+ postgres: "postgresql",
149
+ };
150
+ function assertDatasourceProvider(facts, provider) {
151
+ for (const datasource of facts.datasources) {
152
+ if (datasource.provider === null) {
153
+ continue;
154
+ }
155
+ const named = Object.hasOwn(PSL_PROVIDERS, datasource.provider)
156
+ ? PSL_PROVIDERS[datasource.provider]
157
+ : undefined;
158
+ if (named === provider) {
159
+ continue;
160
+ }
161
+ const where = `${datasource.site.file}:${datasource.site.line}`;
162
+ throw new AvPrismaGenerateStepError(named === undefined
163
+ ? `the schema's datasource \`${datasource.name}\` (${where}) names provider "${datasource.provider}", which this adapter does not support (${Object.keys(PSL_PROVIDERS).join(", ")}).`
164
+ : `--provider ${provider} contradicts the schema's datasource \`${datasource.name}\` (${where}), which names provider "${datasource.provider}"; pass --provider ${named}.`);
165
+ }
166
+ }
129
167
  function assertNothingUnhandled(facts) {
130
168
  if (facts.unhandled.length === 0) {
131
169
  return;
@@ -141,6 +179,17 @@ export async function runPrismaGenerateStep(request) {
141
179
  const prismaVersion = await toolchain.installedPrismaVersion();
142
180
  assertPinnedMajor(prismaVersion);
143
181
  assertClientRuntimeResolves(toolchain, request.clientDirectory);
182
+ const declared = await toolchain
183
+ .readSchema(request.schema)
184
+ .catch((error) => {
185
+ if (error instanceof AvPrismaSchemaReaderError) {
186
+ return undefined;
187
+ }
188
+ throw error;
189
+ });
190
+ if (declared !== undefined) {
191
+ assertDatasourceProvider(declared, request.provider);
192
+ }
144
193
  await toolchain.runPrismaGenerate(request.schema);
145
194
  const facts = await toolchain.readSchema(request.schema);
146
195
  assertNothingUnhandled(facts);
@@ -170,19 +219,17 @@ export async function runPrismaGenerateStep(request) {
170
219
  client: clientShapeOf(surface),
171
220
  };
172
221
  const modelHash = await computeModelHash(model);
173
- let computedFields;
174
- if (request.config !== undefined) {
175
- if (!(await describesModel(request.artifactFile, modelHash))) {
176
- await toolchain.writeArtifact(request.artifactFile, await emitDiscoveryArtifactModule({
177
- ...emission,
178
- computedFields: null,
179
- }));
180
- }
181
- computedFields = (toolchain.readComputedFields ?? readConfigComputedFields)(request.config);
222
+ if (request.config !== undefined &&
223
+ (await stat(request.config).catch(() => undefined))?.isFile() !== true) {
224
+ throw new AvPrismaGenerateStepError(`the configuration \`${request.config}\` does not exist (--config).`);
182
225
  }
183
226
  const source = await emitDiscoveryArtifactModule({
184
227
  ...emission,
185
- computedFields: computedFields?.kind === "read" ? computedFields.computedFields : null,
228
+ computedFields: request.config === undefined
229
+ ? null
230
+ : {
231
+ specifier: computedFieldsSpecifier(request.artifactFile),
232
+ },
186
233
  });
187
234
  await toolchain.writeArtifact(request.artifactFile, source);
188
235
  return {
@@ -192,7 +239,6 @@ export async function runPrismaGenerateStep(request) {
192
239
  admitted,
193
240
  excluded,
194
241
  exemptions,
195
- ...(computedFields === undefined ? {} : { computedFields }),
196
242
  };
197
243
  }
198
244
  function clientShapeOf(surface) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type { AvPrismaAdapterModel, AvPrismaDiscoveryArtifact, AvPrismaReferentialAction, AvPrismaReferentialActionTable, AvPrismaReferentialEdge, AvPrismaResourceReferentialActions, } from "./discovery/discovery-artifact.interface.js";
2
2
  export type { AvPrismaAdapterOptions } from "./prisma.adapter.js";
3
3
  export { createPrismaAdapter } from "./prisma.adapter.js";
4
+ export { describePrismaAdapter } from "./prisma-adapter.description.js";
4
5
  export type { AvPrismaDelegate, AvPrismaDelegateHost, AvPrismaDelegateIndex, AvPrismaTransactionOptions, } from "./prisma-client.interface.js";
5
6
  export type { AvPrismaDriver, AvPrismaIsolationLevel, AvPrismaProvider, AvPrismaProviderProfile, } from "./provider/provider-profile.js";
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { createPrismaAdapter } from "./prisma.adapter.js";
2
+ export { describePrismaAdapter } from "./prisma-adapter.description.js";
@@ -0,0 +1,42 @@
1
+ import type { AvAdapter, AvTransactionCapability } from "@aventara/core";
2
+ import type { AvPrismaAdapterModel, AvPrismaDiscoveryArtifact } from "./discovery/discovery-artifact.interface.js";
3
+ import { type AvPrismaDriver, type AvPrismaProvider } from "./provider/provider-profile.js";
4
+ /** What {@link describePrismaAdapter} takes: the artifact and the declared provider and driver. */
5
+ type PrismaAdapterDescriptionInput<M extends AvPrismaAdapterModel> = {
6
+ /** The `discovery` export of the generated discovery artifact. */
7
+ readonly discovery: AvPrismaDiscoveryArtifact<M>;
8
+ /** The database provider the client was generated for. */
9
+ readonly provider: AvPrismaProvider;
10
+ /** The Prisma driver adapter the client is constructed with. */
11
+ readonly driver: AvPrismaDriver;
12
+ };
13
+ /** The adapter's passive facts, as {@link describePrismaAdapter} states them. */
14
+ type PrismaAdapterDescription<M extends AvPrismaAdapterModel> = PrismaAdapterDescriptionInput<M> & Required<Pick<AvAdapter<M>, "model" | "referentialActions">> & Pick<AvAdapter<M>, "computedFields"> & {
15
+ /**
16
+ * The driver's transaction capability: `"interactive"` when the
17
+ * constructed Adapter runs transactions, `"none"` when it carries no
18
+ * `transaction` (`@prisma/adapter-d1`).
19
+ */
20
+ readonly transactions: AvTransactionCapability;
21
+ /** Every Prisma driver runs developer SQL statements. */
22
+ readonly sqlStatements: true;
23
+ };
24
+ /**
25
+ * The Prisma 7 adapter's passive description: the model, the driver's
26
+ * transaction capability, the referential actions and the computed fields, read
27
+ * from the discovery artifact with no client. `aventara-build` compiles the
28
+ * Contracts from it; `createPrismaAdapter({ ...description, client })` builds
29
+ * the adapter from it.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * // src/aventara.adapter.ts
34
+ * export default describePrismaAdapter({
35
+ * discovery,
36
+ * provider: "postgresql",
37
+ * driver: "@prisma/adapter-pg",
38
+ * });
39
+ * ```
40
+ */
41
+ export declare function describePrismaAdapter<M extends AvPrismaAdapterModel>(input: PrismaAdapterDescriptionInput<M>): PrismaAdapterDescription<M>;
42
+ export {};
@@ -0,0 +1,36 @@
1
+ import { resolveProviderProfile, } from "./provider/provider-profile.js";
2
+ const DELETE_EFFECTS = {
3
+ cascade: "cascade",
4
+ restrict: "restrict",
5
+ "no-action": "noAction",
6
+ "set-null": "setNull",
7
+ "set-default": "setDefault",
8
+ };
9
+ function translateReferentialActions(table) {
10
+ return Object.fromEntries(Object.entries(table).map(([resource, { owningEdges }]) => [
11
+ resource,
12
+ owningEdges.map((edge) => ({
13
+ field: edge.field,
14
+ target: edge.target,
15
+ references: edge.references,
16
+ onDelete: DELETE_EFFECTS[edge.onDelete],
17
+ })),
18
+ ]));
19
+ }
20
+ export function describePrismaAdapter(input) {
21
+ const profile = resolveProviderProfile({
22
+ provider: input.provider,
23
+ driver: input.driver,
24
+ });
25
+ const computedFields = input.discovery.computedFields;
26
+ return Object.freeze({
27
+ discovery: input.discovery,
28
+ provider: input.provider,
29
+ driver: input.driver,
30
+ model: input.discovery.model,
31
+ transactions: profile.transactions,
32
+ referentialActions: translateReferentialActions(input.discovery.referentialActions),
33
+ ...(computedFields === null ? {} : { computedFields }),
34
+ sqlStatements: true,
35
+ });
36
+ }
@@ -5,25 +5,8 @@ import { derivePrismaBackReferences } from "./errors/back-reference.locator.js";
5
5
  import { createPrismaOperationExecutor } from "./execution/operation.executor.js";
6
6
  import { createPrismaSqlStatementExecutor } from "./execution/sql-statement.executor.js";
7
7
  import { createPrismaTransactionExecutor } from "./execution/transaction.executor.js";
8
- import { resolveBigintBinding, resolveProviderProfile, } from "./provider/provider-profile.js";
9
- const DELETE_EFFECTS = {
10
- cascade: "cascade",
11
- restrict: "restrict",
12
- "no-action": "noAction",
13
- "set-null": "setNull",
14
- "set-default": "setDefault",
15
- };
16
- function translateReferentialActions(table) {
17
- return Object.fromEntries(Object.entries(table).map(([resource, { owningEdges }]) => [
18
- resource,
19
- owningEdges.map((edge) => ({
20
- field: edge.field,
21
- target: edge.target,
22
- references: edge.references,
23
- onDelete: DELETE_EFFECTS[edge.onDelete],
24
- })),
25
- ]));
26
- }
8
+ import { describePrismaAdapter } from "./prisma-adapter.description.js";
9
+ import { resolveBigintBinding, } from "./provider/provider-profile.js";
27
10
  const PRISMA_VERSION_UNCHECKED_WARNING = "AVENTARA_PRISMA_VERSION_UNCHECKED";
28
11
  function installedClientVersion(discovery, override) {
29
12
  if (override !== undefined) {
@@ -47,22 +30,19 @@ function installedClientVersion(discovery, override) {
47
30
  return undefined;
48
31
  }
49
32
  export async function createPrismaAdapter(options) {
50
- const profile = resolveProviderProfile({
51
- provider: options.provider,
52
- driver: options.driver,
53
- });
33
+ const description = describePrismaAdapter(options);
54
34
  await loadDiscoveryArtifact({
55
35
  artifact: options.discovery,
56
36
  installedPrismaVersion: installedClientVersion(options.discovery, options.installedPrismaVersion),
57
37
  });
58
38
  assertClientMatchesArtifact(options.discovery.client, options.client);
59
- const model = options.discovery.model;
60
- const referentialActions = translateReferentialActions(options.discovery.referentialActions);
39
+ const model = description.model;
40
+ const referentialActions = description.referentialActions;
61
41
  const backReferences = derivePrismaBackReferences(options.discovery.model);
62
42
  const bigintBinding = resolveBigintBinding(options.driver);
63
- const computedFields = options.discovery.computedFields === null
43
+ const computedFields = description.computedFields === undefined
64
44
  ? {}
65
- : { computedFields: options.discovery.computedFields };
45
+ : { computedFields: description.computedFields };
66
46
  const { execute } = createPrismaOperationExecutor({
67
47
  client: options.client,
68
48
  model: options.discovery.model,
@@ -75,13 +55,13 @@ export async function createPrismaAdapter(options) {
75
55
  : { transactionOptions: options.transactionOptions }),
76
56
  });
77
57
  const executeSql = createPrismaSqlStatementExecutor(options.client);
78
- if (profile.transactions === "none") {
58
+ if (description.transactions === "none") {
79
59
  return {
80
60
  model,
81
61
  referentialActions,
82
62
  ...computedFields,
83
63
  execute,
84
- sqlStatements: true,
64
+ sqlStatements: description.sqlStatements,
85
65
  executeSql,
86
66
  };
87
67
  }
@@ -101,7 +81,7 @@ export async function createPrismaAdapter(options) {
101
81
  ...computedFields,
102
82
  execute,
103
83
  transaction,
104
- sqlStatements: true,
84
+ sqlStatements: description.sqlStatements,
105
85
  executeSql,
106
86
  };
107
87
  }
@@ -131,7 +131,7 @@ function isUniqueSelectorVariant(key) {
131
131
  function singleCallArguments(context, key, predicate, projectionArgs) {
132
132
  const unique = isUniqueSelectorVariant(key);
133
133
  const where = unique
134
- ? { where: uniqueWhere(context) }
134
+ ? { where: uniqueWhere(context, key) }
135
135
  : predicate === undefined
136
136
  ? {}
137
137
  : { where: predicate };
@@ -189,11 +189,18 @@ function singleCallArguments(context, key, predicate, projectionArgs) {
189
189
  throw new AvPrismaOperationTranslationError(`"${key}" has a dispatch entry and no argument shape, which is a translator defect`);
190
190
  }
191
191
  }
192
- function uniqueWhere(context) {
193
- const selector = context.args.where;
194
- if (!isRecord(selector)) {
192
+ const SCOPED_UNIQUE_VARIANTS = new Set([
193
+ "update.unique",
194
+ "delete.unique",
195
+ ]);
196
+ function uniqueWhere(context, key) {
197
+ const where = context.args.where;
198
+ if (!isRecord(where)) {
195
199
  throw new AvPrismaOperationTranslationError("where must be a unique identifier object for a unique variant");
196
200
  }
201
+ const { AND: conditions, ...rest } = where;
202
+ const scoped = conditions !== undefined && SCOPED_UNIQUE_VARIANTS.has(key);
203
+ const selector = scoped ? rest : where;
197
204
  const addressed = addressIdentifier({
198
205
  addresses: identifiersOf(context, context.resource),
199
206
  values: selector,
@@ -203,7 +210,17 @@ function uniqueWhere(context) {
203
210
  if (addressed === undefined) {
204
211
  throw new AvPrismaOperationTranslationError(`where: { ${Object.keys(selector).join(", ")} } is no identifier of Resource "${context.resource}", so it addresses no row`);
205
212
  }
206
- return addressed.where;
213
+ if (!scoped) {
214
+ return addressed.where;
215
+ }
216
+ return {
217
+ ...addressed.where,
218
+ ...translateWhere({
219
+ model: context.model,
220
+ resource: context.resource,
221
+ where: { AND: conditions },
222
+ }),
223
+ };
207
224
  }
208
225
  function identifierSelect(context) {
209
226
  const [address] = identifiersOf(context, context.resource);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/prisma7-adapter",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.4",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "The Prisma 7 adapter for Aventara.",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "CHANGELOG.md"
31
31
  ],
32
32
  "dependencies": {
33
- "@aventara/core": "1.0.0-rc.2"
33
+ "@aventara/core": "1.0.0-rc.4"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@prisma/adapter-better-sqlite3": "^7.10.0",
@@ -1,19 +0,0 @@
1
- import type { AvConfigComputedFields } from "@aventara/core";
2
- /** What the reader made of a configuration: its computed fields, or why it could not tell. */
3
- export type ConfigComputedFieldsReading = {
4
- readonly kind: "read";
5
- readonly computedFields: AvConfigComputedFields;
6
- } | {
7
- readonly kind: "unread";
8
- readonly reason: string;
9
- };
10
- /** Thrown when the configuration the step was told to read cannot be found. */
11
- export declare class AvPrismaConfigReaderError extends Error {
12
- readonly name = "AvPrismaConfigReaderError";
13
- }
14
- /**
15
- * Reads which fields the configuration in `configFile` computes, from the type
16
- * of its `aventaraConfig` export (a function's awaited return, or a value), or
17
- * of its default export.
18
- */
19
- export declare function readConfigComputedFields(configFile: string): ConfigComputedFieldsReading;
@@ -1,210 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import path from "node:path";
3
- import ts from "typescript";
4
- export class AvPrismaConfigReaderError extends Error {
5
- name = "AvPrismaConfigReaderError";
6
- }
7
- const CONFIG_EXPORTS = ["aventaraConfig", "default"];
8
- const LAYERS = ["application", "client", "agent"];
9
- const AGENT_PARENTS = ["root", "application", "client"];
10
- const PHASES = ["create", "update"];
11
- class NotExact extends Error {
12
- }
13
- function defaultOptions() {
14
- return {
15
- target: ts.ScriptTarget.ES2022,
16
- module: ts.ModuleKind.NodeNext,
17
- moduleResolution: ts.ModuleResolutionKind.NodeNext,
18
- strict: true,
19
- };
20
- }
21
- function compilerOptionsFor(configFile) {
22
- const tsconfig = ts.findConfigFile(path.dirname(configFile), ts.sys.fileExists, "tsconfig.json");
23
- if (tsconfig === undefined) {
24
- return defaultOptions();
25
- }
26
- const read = ts.readConfigFile(tsconfig, ts.sys.readFile);
27
- if (read.error !== undefined) {
28
- return defaultOptions();
29
- }
30
- return ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(tsconfig)).options;
31
- }
32
- export function readConfigComputedFields(configFile) {
33
- const file = path.resolve(configFile);
34
- if (!existsSync(file)) {
35
- throw new AvPrismaConfigReaderError(`the configuration \`${configFile}\` does not exist (--config).`);
36
- }
37
- const program = ts.createProgram({
38
- rootNames: [file],
39
- options: {
40
- ...compilerOptionsFor(file),
41
- noEmit: true,
42
- skipLibCheck: true,
43
- },
44
- });
45
- const checker = program.getTypeChecker();
46
- const source = program.getSourceFile(file);
47
- const module = source === undefined ? undefined : checker.getSymbolAtLocation(source);
48
- if (module === undefined) {
49
- throw new AvPrismaConfigReaderError(`the configuration \`${configFile}\` is not a module: it exports nothing to read.`);
50
- }
51
- const exports = checker.getExportsOfModule(module);
52
- const exported = CONFIG_EXPORTS.map((name) => exports.find((symbol) => symbol.escapedName === name)).find((symbol) => symbol !== undefined);
53
- if (exported === undefined) {
54
- throw new AvPrismaConfigReaderError(`the configuration \`${configFile}\` exports neither \`aventaraConfig\` nor a default.`);
55
- }
56
- const target = exported.flags & ts.SymbolFlags.Alias
57
- ? checker.getAliasedSymbol(exported)
58
- : exported;
59
- const declared = checker.getTypeOfSymbol(target);
60
- const [signature] = declared.getCallSignatures();
61
- const config = signature === undefined
62
- ? declared
63
- : (checker.getAwaitedType(signature.getReturnType()) ??
64
- signature.getReturnType());
65
- try {
66
- return {
67
- kind: "read",
68
- computedFields: computedFieldsOfType(checker, config),
69
- };
70
- }
71
- catch (error) {
72
- if (error instanceof NotExact) {
73
- return { kind: "unread", reason: error.message };
74
- }
75
- throw error;
76
- }
77
- }
78
- function computedFieldsOfType(checker, config) {
79
- assertExact(checker, config, "the configuration");
80
- const overlays = Object.fromEntries(LAYERS.map((layer) => {
81
- const overlay = stated(checker, config, layer, "the configuration");
82
- return [
83
- layer,
84
- overlay === undefined
85
- ? {}
86
- : layerComputedFields(checker, overlay, layer),
87
- ];
88
- }));
89
- return {
90
- agent: overlays.agent,
91
- agentExtends: agentParent(checker, config),
92
- application: overlays.application,
93
- client: overlays.client,
94
- root: layerComputedFields(checker, config, "the root"),
95
- };
96
- }
97
- function agentParent(checker, config) {
98
- const agent = stated(checker, config, "agent", "the configuration");
99
- const property = agent?.getProperty("extends");
100
- if (property !== undefined && property.flags & ts.SymbolFlags.Optional) {
101
- throw new NotExact("`extends` in agent may be absent");
102
- }
103
- const parent = property === undefined ? undefined : checker.getTypeOfSymbol(property);
104
- if (parent === undefined) {
105
- return "client";
106
- }
107
- const named = AGENT_PARENTS.find((candidate) => parent.isStringLiteral() && parent.value === candidate);
108
- if (named === undefined) {
109
- throw new NotExact(`\`agent.extends\` is \`${checker.typeToString(parent)}\`, not one layer`);
110
- }
111
- return named;
112
- }
113
- function layerComputedFields(checker, layer, where) {
114
- const resources = stated(checker, layer, "resources", where);
115
- if (resources === undefined) {
116
- return {};
117
- }
118
- const result = {};
119
- for (const [resource, block] of members(checker, resources, `${where}'s resources`)) {
120
- const fields = stated(checker, block, "fields", `resources.${resource}`);
121
- if (fields === undefined) {
122
- continue;
123
- }
124
- const phases = {
125
- create: [],
126
- update: [],
127
- };
128
- for (const [field, entry] of members(checker, fields, `resources.${resource}.fields`)) {
129
- const at = `resources.${resource}.fields.${field}`;
130
- const pipelines = stated(checker, entry, "pipelines", at);
131
- const compute = pipelines === undefined
132
- ? undefined
133
- : stated(checker, pipelines, "compute", `${at}.pipelines`);
134
- if (compute === undefined) {
135
- continue;
136
- }
137
- for (const phase of PHASES) {
138
- if (stagePresent(checker, compute, phase, `${at}.pipelines.compute`)) {
139
- phases[phase].push(field);
140
- }
141
- }
142
- }
143
- const create = phases.create.sort();
144
- const update = phases.update.sort();
145
- if (create.length > 0 || update.length > 0) {
146
- result[resource] = {
147
- ...(create.length === 0 ? {} : { create }),
148
- ...(update.length === 0 ? {} : { update }),
149
- };
150
- }
151
- }
152
- return Object.fromEntries(Object.entries(result).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
153
- }
154
- function assertExact(checker, type, where) {
155
- if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
156
- throw new NotExact(`${where} has no type (\`${checker.typeToString(type)}\`)`);
157
- }
158
- if (type.isUnion()) {
159
- throw new NotExact(`${where} is one of several shapes (\`${checker.typeToString(type)}\`)`);
160
- }
161
- if (checker.getIndexInfosOfType(type).length > 0) {
162
- throw new NotExact(`${where} is not a literal: it has an index signature`);
163
- }
164
- }
165
- function stated(checker, type, name, where) {
166
- const property = type.getProperty(name);
167
- if (property === undefined) {
168
- return undefined;
169
- }
170
- if (property.flags & ts.SymbolFlags.Optional) {
171
- throw new NotExact(`\`${name}\` in ${where} may be absent`);
172
- }
173
- const value = checker.getTypeOfSymbol(property);
174
- if (value.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Never)) {
175
- return undefined;
176
- }
177
- assertExact(checker, value, `\`${name}\` in ${where}`);
178
- return value;
179
- }
180
- function stagePresent(checker, compute, phase, where) {
181
- const property = compute.getProperty(phase);
182
- if (property === undefined) {
183
- return false;
184
- }
185
- if (property.flags & ts.SymbolFlags.Optional) {
186
- throw new NotExact(`\`${phase}\` in ${where} may be absent`);
187
- }
188
- const stage = checker.getTypeOfSymbol(property);
189
- if (stage.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Never)) {
190
- return false;
191
- }
192
- const undefinedMember = (candidate) => (candidate.flags & ts.TypeFlags.Undefined) !== 0;
193
- if (stage.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown) ||
194
- (stage.isUnion() && stage.types.some(undefinedMember))) {
195
- throw new NotExact(`\`${phase}\` in ${where} may be undefined (\`${checker.typeToString(stage)}\`)`);
196
- }
197
- return true;
198
- }
199
- function members(checker, type, where) {
200
- return type
201
- .getProperties()
202
- .map((property) => {
203
- const name = property.getName();
204
- const value = stated(checker, type, name, where);
205
- return value === undefined
206
- ? undefined
207
- : [name, value];
208
- })
209
- .filter((entry) => entry !== undefined);
210
- }