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

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +65 -18
  3. package/dist/discovery/artifact.loader.d.ts +6 -2
  4. package/dist/discovery/artifact.loader.js +6 -1
  5. package/dist/discovery/capability.deriver.d.ts +4 -1
  6. package/dist/discovery/capability.deriver.js +38 -3
  7. package/dist/discovery/discovery-artifact.interface.d.ts +10 -2
  8. package/dist/discovery/discovery-artifact.interface.js +1 -1
  9. package/dist/discovery/installed-client.locator.d.ts +13 -0
  10. package/dist/discovery/installed-client.locator.js +43 -0
  11. package/dist/discovery/model.discoverer.d.ts +3 -0
  12. package/dist/discovery/model.discoverer.js +14 -2
  13. package/dist/errors/back-reference.locator.d.ts +25 -0
  14. package/dist/errors/back-reference.locator.js +100 -0
  15. package/dist/errors/bigint-binding.locator.d.ts +7 -0
  16. package/dist/errors/bigint-binding.locator.js +27 -0
  17. package/dist/errors/error.normalizer.d.ts +34 -2
  18. package/dist/errors/error.normalizer.js +69 -2
  19. package/dist/errors/prisma-error.reader.d.ts +4 -0
  20. package/dist/errors/prisma-error.reader.js +2 -0
  21. package/dist/execution/operation.executor.d.ts +6 -0
  22. package/dist/execution/operation.executor.js +21 -8
  23. package/dist/execution/transaction.executor.d.ts +4 -0
  24. package/dist/execution/transaction.executor.js +2 -0
  25. package/dist/generate/artifact.emitter.d.ts +6 -0
  26. package/dist/generate/artifact.emitter.js +56 -2
  27. package/dist/generate/computed-fields.reader.d.ts +19 -0
  28. package/dist/generate/computed-fields.reader.js +210 -0
  29. package/dist/generate/declared-support.reader.d.ts +1 -1
  30. package/dist/generate/discovery.cli.d.ts +1 -0
  31. package/dist/generate/discovery.cli.js +20 -4
  32. package/dist/generate/index.d.ts +8 -0
  33. package/dist/generate/index.js +3 -0
  34. package/dist/generate/prisma.generate.d.ts +17 -0
  35. package/dist/generate/prisma.generate.js +41 -3
  36. package/dist/prisma.adapter.d.ts +4 -4
  37. package/dist/prisma.adapter.js +37 -2
  38. package/dist/provider/provider-profile.d.ts +19 -3
  39. package/dist/provider/provider-profile.js +21 -0
  40. package/dist/translation/json-null.sentinel.d.ts +7 -0
  41. package/dist/translation/json-null.sentinel.js +17 -0
  42. package/dist/translation/mutation.translator.js +7 -0
  43. package/dist/translation/where.translator.js +100 -17
  44. package/package.json +6 -6
@@ -0,0 +1,100 @@
1
+ export function derivePrismaBackReferences(model) {
2
+ const table = {};
3
+ for (const [source, resource] of Object.entries(model.resources)) {
4
+ const entries = {};
5
+ for (const [field, shape] of Object.entries(resource.fields)) {
6
+ if (shape.kind !== "relation" || typeof shape.inverse !== "string") {
7
+ continue;
8
+ }
9
+ const relation = shape.inverse;
10
+ const other = model.resources[shape.target]?.fields[relation];
11
+ entries[field] = {
12
+ relation,
13
+ references: other?.kind === "relation" ? (other.referenceFields ?? []) : [],
14
+ };
15
+ }
16
+ table[source] = entries;
17
+ }
18
+ return table;
19
+ }
20
+ const DATA_KEYS = {
21
+ $create: null,
22
+ $connectOrCreate: ["create"],
23
+ $update: ["data"],
24
+ $upsert: ["create", "update"],
25
+ };
26
+ const TOP_DATA_KEYS = ["data", "create", "update"];
27
+ export function locateBackReferenceWrite(table, model, resource, args) {
28
+ if (!isRecord(args)) {
29
+ return undefined;
30
+ }
31
+ for (const key of TOP_DATA_KEYS) {
32
+ const found = eachRecord(args[key], [key], (data, path) => inRecord(table, model, resource, data, path, undefined));
33
+ if (found !== undefined) {
34
+ return found;
35
+ }
36
+ }
37
+ return undefined;
38
+ }
39
+ function inRecord(table, model, resource, data, path, via) {
40
+ const fields = model.resources[resource]?.fields;
41
+ for (const [key, value] of Object.entries(data)) {
42
+ if (via !== undefined &&
43
+ (key === via.relation || via.references.includes(key))) {
44
+ return [...path, key];
45
+ }
46
+ const shape = fields?.[key];
47
+ if (shape?.kind !== "relation" || !isRecord(value)) {
48
+ continue;
49
+ }
50
+ const back = table[resource]?.[key];
51
+ for (const [directive, operand] of Object.entries(value)) {
52
+ const dataKeys = DATA_KEYS[directive];
53
+ if (dataKeys === undefined) {
54
+ continue;
55
+ }
56
+ const found = eachRecord(operand, [...path, key, directive], (item, itemPath) => dataKeys === null
57
+ ? inRecord(table, model, shape.target, item, itemPath, back)
58
+ : firstOf(dataKeys, (dataKey) => {
59
+ const nested = item[dataKey];
60
+ return isRecord(nested)
61
+ ? inRecord(table, model, shape.target, nested, [...itemPath, dataKey], back)
62
+ : undefined;
63
+ }));
64
+ if (found !== undefined) {
65
+ return found;
66
+ }
67
+ }
68
+ }
69
+ return undefined;
70
+ }
71
+ function eachRecord(value, path, visit) {
72
+ if (Array.isArray(value)) {
73
+ for (const [index, entry] of value.entries()) {
74
+ const found = isRecord(entry)
75
+ ? visit(entry, [...path, index])
76
+ : undefined;
77
+ if (found !== undefined) {
78
+ return found;
79
+ }
80
+ }
81
+ return undefined;
82
+ }
83
+ return isRecord(value) ? visit(value, path) : undefined;
84
+ }
85
+ function firstOf(keys, find) {
86
+ for (const key of keys) {
87
+ const found = find(key);
88
+ if (found !== undefined) {
89
+ return found;
90
+ }
91
+ }
92
+ return undefined;
93
+ }
94
+ function isRecord(value) {
95
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
96
+ return false;
97
+ }
98
+ const prototype = Object.getPrototypeOf(value);
99
+ return prototype === Object.prototype || prototype === null;
100
+ }
@@ -0,0 +1,7 @@
1
+ import type { PrismaArgumentPath } from "./back-reference.locator.js";
2
+ /**
3
+ * Where the caller's arguments hold a `bigint` beyond ±(2^53 − 1), as a path in
4
+ * the arguments; `undefined` when they hold none. The first such value, in key
5
+ * order.
6
+ */
7
+ export declare function locateUnboundBigint(args: unknown): PrismaArgumentPath | undefined;
@@ -0,0 +1,27 @@
1
+ export function locateUnboundBigint(args) {
2
+ return isRecord(args) ? inValue(args, []) : undefined;
3
+ }
4
+ function inValue(value, path) {
5
+ if (typeof value === "bigint") {
6
+ return Number.isSafeInteger(Number(value)) ? undefined : path;
7
+ }
8
+ const entries = Array.isArray(value)
9
+ ? [...value.entries()]
10
+ : isRecord(value)
11
+ ? Object.entries(value)
12
+ : [];
13
+ for (const [key, member] of entries) {
14
+ const found = inValue(member, [...path, key]);
15
+ if (found !== undefined) {
16
+ return found;
17
+ }
18
+ }
19
+ return undefined;
20
+ }
21
+ function isRecord(value) {
22
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
23
+ return false;
24
+ }
25
+ const prototype = Object.getPrototypeOf(value);
26
+ return prototype === Object.prototype || prototype === null;
27
+ }
@@ -1,11 +1,32 @@
1
1
  import { AvError, type AvOperationErrorCode, type AvOperationIdentity } from "@aventara/core";
2
+ import { type AvDatabaseRefusal } from "@aventara/core/internal";
3
+ import type { PrismaArgumentPath } from "./back-reference.locator.js";
2
4
  import { type PrismaConstraintSubject } from "./prisma-error.reader.js";
3
- /** The three answers a Prisma failure can get. */
5
+ /**
6
+ * Where in the caller's arguments a refused call's cause stands, read only after
7
+ * the refusal; each answers `undefined` when the arguments do not explain it.
8
+ */
9
+ export type PrismaArgumentLocators = {
10
+ /** A write that writes its relation back: for a validation error. */
11
+ readonly backReference?: () => PrismaArgumentPath | undefined;
12
+ /** A bigint the driver cannot bind: given only for a driver that binds a safe integer. */
13
+ readonly unboundBigint?: () => PrismaArgumentPath | undefined;
14
+ };
15
+ /** A value the database refused: every refusal but the ORM's nested-write one. */
16
+ export type PrismaValueRefusal = Exclude<AvDatabaseRefusal, "backReference">;
17
+ /** The five answers a Prisma failure can get. */
4
18
  export type PrismaErrorClassification = {
5
19
  readonly kind: "absence";
6
20
  } | {
7
21
  readonly kind: "outcome";
8
22
  readonly code: AvOperationErrorCode;
23
+ } | {
24
+ readonly kind: "value";
25
+ readonly refusal: PrismaValueRefusal;
26
+ readonly path: PrismaArgumentPath;
27
+ } | {
28
+ readonly kind: "nested-write";
29
+ readonly path: PrismaArgumentPath;
9
30
  } | {
10
31
  readonly kind: "unclassified";
11
32
  };
@@ -23,8 +44,15 @@ export type PrismaErrorTable = Readonly<Record<string, PrismaErrorRule>>;
23
44
  export declare const PRISMA_ERROR_TABLE_MAJOR = "7";
24
45
  /** Every code here was induced above. */
25
46
  export declare const PRISMA_ERROR_TABLE: PrismaErrorTable;
47
+ /** Every P-code here was induced above: a value the database refused, by what it refused. */
48
+ export declare const PRISMA_VALUE_TABLE: Readonly<Record<string, PrismaValueRefusal>>;
49
+ /**
50
+ * `P2039` with the driver adapter's `kind: "postgres"`, by PostgreSQL's SQLSTATE:
51
+ * each one induced above by a caller's value.
52
+ */
53
+ export declare const PRISMA_DATABASE_VALUE_TABLE: Readonly<Record<string, PrismaValueRefusal>>;
26
54
  /** Classifies one failed Prisma call. */
27
- export declare function classifyPrismaError(error: unknown, operation: Pick<AvOperationIdentity, "family" | "variant">): PrismaErrorClassification;
55
+ export declare function classifyPrismaError(error: unknown, operation: Pick<AvOperationIdentity, "family" | "variant">, locators?: PrismaArgumentLocators): PrismaErrorClassification;
28
56
  /**
29
57
  * Classifies one failed `appTx.$sql` statement: a `P2010` by its driver-adapter
30
58
  * kind, any other P-code by the table, never forgiven as an absence (a statement
@@ -33,6 +61,10 @@ export declare function classifyPrismaError(error: unknown, operation: Pick<AvOp
33
61
  export declare function classifyPrismaStatementError(error: unknown): PrismaErrorClassification;
34
62
  /** The deliberate failure an `outcome` classification stands for. */
35
63
  export declare function prismaOutcomeFailure(code: AvOperationErrorCode): AvError;
64
+ /** The `A2004` a value the database refused answers: one issue at the arguments (or the value in them) saying what was refused. */
65
+ export declare function prismaValueFailure(refusal: PrismaValueRefusal, path?: PrismaArgumentPath): AvError;
66
+ /** The `A2004` a nested write that writes its relation back answers: `V1012` at `path`. */
67
+ export declare function prismaNestedWriteFailure(path: PrismaArgumentPath): AvError;
36
68
  export type PrismaConflictSubject = {
37
69
  readonly code: string;
38
70
  readonly model: string | undefined;
@@ -1,4 +1,5 @@
1
1
  import { AvError, } from "@aventara/core";
2
+ import { DATABASE_REFUSALS, } from "@aventara/core/internal";
2
3
  import { readPrismaError, } from "./prisma-error.reader.js";
3
4
  export const PRISMA_ERROR_TABLE_MAJOR = "7";
4
5
  export const PRISMA_ERROR_TABLE = {
@@ -14,11 +15,40 @@ export const PRISMA_ERROR_TABLE = {
14
15
  subject: "root-record",
15
16
  },
16
17
  },
18
+ P2028: { outcome: "A3002" },
17
19
  };
20
+ export const PRISMA_VALUE_TABLE = {
21
+ P2000: "tooLong",
22
+ P2020: "outOfRange",
23
+ P2007: "invalidInput",
24
+ P2011: "computedNull",
25
+ };
26
+ export const PRISMA_DATABASE_VALUE_TABLE = {
27
+ "22021": "nulInText",
28
+ "22P05": "nulInJson",
29
+ "22025": "trailingEscape",
30
+ "22009": "datetimeOutOfRange",
31
+ "22012": "divisionByZero",
32
+ };
33
+ function refusedValue(facts) {
34
+ if (facts.code === undefined) {
35
+ return undefined;
36
+ }
37
+ if (Object.hasOwn(PRISMA_VALUE_TABLE, facts.code)) {
38
+ return PRISMA_VALUE_TABLE[facts.code];
39
+ }
40
+ return facts.code === "P2039" &&
41
+ facts.driverAdapterKind === "postgres" &&
42
+ facts.databaseCode !== undefined &&
43
+ Object.hasOwn(PRISMA_DATABASE_VALUE_TABLE, facts.databaseCode)
44
+ ? PRISMA_DATABASE_VALUE_TABLE[facts.databaseCode]
45
+ : undefined;
46
+ }
18
47
  const OUTCOME_MESSAGES = {
19
48
  A2008: "The operation conflicts with the current state of the resource.",
20
49
  A2013: "Another operation modified the same state concurrently, so this operation was not applied and nothing was written.",
21
50
  A2003: "The operation's arguments referenced a record that does not exist.",
51
+ A3002: "The transaction was no longer open when this call ran: it had expired or been closed.",
22
52
  };
23
53
  function matchesAbsenceSubject(subject, facts) {
24
54
  switch (subject) {
@@ -26,10 +56,25 @@ function matchesAbsenceSubject(subject, facts) {
26
56
  return facts.relation === undefined;
27
57
  }
28
58
  }
29
- export function classifyPrismaError(error, operation) {
59
+ export function classifyPrismaError(error, operation, locators = {}) {
30
60
  const facts = readPrismaError(error);
31
61
  if (facts.code === undefined) {
32
- return { kind: "unclassified" };
62
+ const path = facts.errorClass === PRISMA_VALIDATION_ERROR_CLASS
63
+ ? locators.backReference?.()
64
+ : undefined;
65
+ return path === undefined
66
+ ? { kind: "unclassified" }
67
+ : { kind: "nested-write", path };
68
+ }
69
+ const refusal = refusedValue(facts);
70
+ if (refusal !== undefined) {
71
+ return { kind: "value", refusal, path: [] };
72
+ }
73
+ if (isDriverRefusalWithoutDatabaseCode(facts)) {
74
+ const path = locators.unboundBigint?.();
75
+ if (path !== undefined) {
76
+ return { kind: "value", refusal: "outOfRange", path };
77
+ }
33
78
  }
34
79
  const rule = Object.hasOwn(PRISMA_ERROR_TABLE, facts.code)
35
80
  ? PRISMA_ERROR_TABLE[facts.code]
@@ -74,6 +119,28 @@ export function prismaOutcomeFailure(code) {
74
119
  message: OUTCOME_MESSAGES[code] ?? "The operation could not be completed.",
75
120
  });
76
121
  }
122
+ export function prismaValueFailure(refusal, path = []) {
123
+ const { code, message } = DATABASE_REFUSALS[refusal];
124
+ return new AvError("A2004", {
125
+ message: VALUE_REFUSED_MESSAGE,
126
+ issues: [{ path: ["arguments", ...path], code, message }],
127
+ });
128
+ }
129
+ const VALUE_REFUSED_MESSAGE = "The database refused a value in the arguments: it cannot store or compare it as written.";
130
+ function isDriverRefusalWithoutDatabaseCode(facts) {
131
+ return (facts.code === "P2039" &&
132
+ facts.driverAdapterKind === "sqlite" &&
133
+ facts.databaseCode === undefined);
134
+ }
135
+ const PRISMA_VALIDATION_ERROR_CLASS = "PrismaClientValidationError";
136
+ export function prismaNestedWriteFailure(path) {
137
+ const { code, message } = DATABASE_REFUSALS.backReference;
138
+ return new AvError("A2004", {
139
+ message: NESTED_WRITE_REFUSED_MESSAGE,
140
+ issues: [{ path: ["arguments", ...path], code, message }],
141
+ });
142
+ }
143
+ const NESTED_WRITE_REFUSED_MESSAGE = "The data source refused a nested write in the arguments.";
77
144
  export function describePrismaConflict(error) {
78
145
  const facts = readPrismaError(error);
79
146
  if (facts.code === undefined) {
@@ -26,7 +26,11 @@ export type PrismaErrorFacts = {
26
26
  readonly constraint: PrismaConstraintSubject | undefined;
27
27
  /** `meta.driverAdapterError.cause.kind`, e.g. `"UniqueConstraintViolation"`. */
28
28
  readonly driverAdapterKind: string | undefined;
29
+ /** `meta.driverAdapterError.cause.originalCode`, e.g. `"22021"`. */
30
+ readonly databaseCode: string | undefined;
29
31
  readonly legacyTarget: readonly string[] | undefined;
32
+ /** The error's own `name`, e.g. `"PrismaClientValidationError"`. */
33
+ readonly errorClass: string | undefined;
30
34
  };
31
35
  /**
32
36
  * Reads one hostile value into the facts a classification may use.
@@ -72,6 +72,8 @@ export function readPrismaError(error) {
72
72
  relatedModel: ownString(meta, "model"),
73
73
  constraint: readConstraint(meta),
74
74
  driverAdapterKind: ownString(ownValue(ownValue(meta, "driverAdapterError"), "cause"), "kind"),
75
+ databaseCode: ownString(ownValue(ownValue(meta, "driverAdapterError"), "cause"), "originalCode"),
75
76
  legacyTarget: ownStringList(meta, "target"),
77
+ errorClass: ownString(error, "name"),
76
78
  };
77
79
  }
@@ -1,7 +1,9 @@
1
1
  import type { AvAdapterModel } from "@aventara/core";
2
2
  import type { AvAdapterOperationExecutor } from "@aventara/core/internal";
3
3
  import type { PrismaResourceTranslation } from "../discovery/discovery-artifact.interface.js";
4
+ import { type PrismaBackReferenceTable } from "../errors/back-reference.locator.js";
4
5
  import { type AvPrismaDelegateHost, type AvPrismaTransactionOptions } from "../prisma-client.interface.js";
6
+ import type { PrismaBigintBinding } from "../provider/provider-profile.js";
5
7
  /** A call the executor cannot issue against the client it was given. */
6
8
  export declare class AvPrismaExecutionError extends Error {
7
9
  readonly name = "AvPrismaExecutionError";
@@ -12,6 +14,10 @@ export type PrismaOperationExecutorDependencies = {
12
14
  readonly model: AvAdapterModel;
13
15
  /** The generate-time translation table (Prisma addressing per Resource). */
14
16
  readonly translation: Readonly<Record<string, PrismaResourceTranslation>>;
17
+ /** Each relation field's back-reference (`errors/back-reference.locator.ts`). */
18
+ readonly backReferences: PrismaBackReferenceTable;
19
+ /** How the driver binds a `bigint` (`provider/provider-profile.ts`). */
20
+ readonly bigintBinding: PrismaBigintBinding;
15
21
  readonly scope: PrismaExecutionScope;
16
22
  readonly transactionOptions?: AvPrismaTransactionOptions;
17
23
  };
@@ -1,4 +1,6 @@
1
- import { classifyPrismaError, prismaOutcomeFailure, } from "../errors/error.normalizer.js";
1
+ import { locateBackReferenceWrite, } from "../errors/back-reference.locator.js";
2
+ import { locateUnboundBigint } from "../errors/bigint-binding.locator.js";
3
+ import { classifyPrismaError, prismaNestedWriteFailure, prismaOutcomeFailure, prismaValueFailure, } from "../errors/error.normalizer.js";
2
4
  import { prismaDelegates, } from "../prisma-client.interface.js";
3
5
  import { PRISMA_VARIANT_DISPATCH, translateOperation, } from "../translation/operation.translator.js";
4
6
  import { runFirstVariantEmulation } from "./first-variant.emulator.js";
@@ -12,6 +14,12 @@ export function delegateKeyOf(model) {
12
14
  }
13
15
  export function createPrismaOperationExecutor(dependencies) {
14
16
  const issue = async (call) => resolveDelegateCall(dependencies.client, call)(call.args);
17
+ const locatorFor = (operation) => ({
18
+ backReference: () => locateBackReferenceWrite(dependencies.backReferences, dependencies.model, operation.resource, operation.arguments),
19
+ ...(dependencies.bigintBinding === "safe-integer"
20
+ ? { unboundBigint: () => locateUnboundBigint(operation.arguments) }
21
+ : {}),
22
+ });
15
23
  return {
16
24
  async execute(operation) {
17
25
  const result = resultKindOf(dispatchOf(operation));
@@ -27,16 +35,17 @@ export function createPrismaOperationExecutor(dependencies) {
27
35
  };
28
36
  if (translation.plan.kind === "single") {
29
37
  const call = translation.plan.call;
30
- return shape(result, await issueClassified(issue, call, operation), shaping);
38
+ return shape(result, await issueClassified(issue, call, operation, locatorFor(operation)), shaping);
31
39
  }
32
40
  const { steps } = translation.plan;
33
- const emulated = await classifyFailure(operation, () => runFirstVariantEmulation({
41
+ const locate = locatorFor(operation);
42
+ const emulated = await classifyFailure(operation, locate, () => runFirstVariantEmulation({
34
43
  steps,
35
44
  identifiers: identifiersOf(dependencies, operation.resource),
36
45
  inUnit: (work) => withUnit(dependencies, (client) => work((call, step) => {
37
46
  const send = (one) => resolveDelegateCall(client, one)(one.args);
38
47
  return step === "read"
39
- ? issueClassified(send, call, operation)
48
+ ? issueClassified(send, call, operation, locate)
40
49
  : send(call);
41
50
  })),
42
51
  }));
@@ -86,20 +95,24 @@ function resolveDelegateCall(client, call) {
86
95
  }
87
96
  return (args) => Promise.resolve(method.call(delegate, args));
88
97
  }
89
- function issueClassified(issue, call, operation) {
90
- return classifyFailure(operation, () => issue(call));
98
+ function issueClassified(issue, call, operation, locate) {
99
+ return classifyFailure(operation, locate, () => issue(call));
91
100
  }
92
- async function classifyFailure(operation, work) {
101
+ async function classifyFailure(operation, locate, work) {
93
102
  try {
94
103
  return await work();
95
104
  }
96
105
  catch (error) {
97
- const classification = classifyPrismaError(error, operation);
106
+ const classification = classifyPrismaError(error, operation, locate);
98
107
  switch (classification.kind) {
99
108
  case "absence":
100
109
  return null;
101
110
  case "outcome":
102
111
  throw prismaOutcomeFailure(classification.code);
112
+ case "value":
113
+ throw prismaValueFailure(classification.refusal, classification.path);
114
+ case "nested-write":
115
+ throw prismaNestedWriteFailure(classification.path);
103
116
  case "unclassified":
104
117
  throw error;
105
118
  }
@@ -1,12 +1,16 @@
1
1
  import type { AvAdapterModel } from "@aventara/core";
2
2
  import type { AvAdapterTransactionExecutor } from "@aventara/core/internal";
3
3
  import type { PrismaResourceTranslation } from "../discovery/discovery-artifact.interface.js";
4
+ import type { PrismaBackReferenceTable } from "../errors/back-reference.locator.js";
4
5
  import type { AvPrismaDelegateHost, AvPrismaTransactionOptions } from "../prisma-client.interface.js";
6
+ import type { PrismaBigintBinding } from "../provider/provider-profile.js";
5
7
  export type PrismaTransactionExecutorDependencies = {
6
8
  readonly client: AvPrismaDelegateHost;
7
9
  readonly model: AvAdapterModel;
8
10
  /** The generate-time translation table (Prisma addressing per Resource). */
9
11
  readonly translation: Readonly<Record<string, PrismaResourceTranslation>>;
12
+ readonly backReferences: PrismaBackReferenceTable;
13
+ readonly bigintBinding: PrismaBigintBinding;
10
14
  readonly transactionOptions?: AvPrismaTransactionOptions;
11
15
  };
12
16
  export type PrismaTransactionRunner<M extends AvAdapterModel> = <T>(work: (executor: AvAdapterTransactionExecutor<M>) => Promise<T>) => Promise<T>;
@@ -7,6 +7,8 @@ export function createPrismaTransactionExecutor(dependencies) {
7
7
  client: transactional,
8
8
  model: dependencies.model,
9
9
  translation: dependencies.translation,
10
+ backReferences: dependencies.backReferences,
11
+ bigintBinding: dependencies.bigintBinding,
10
12
  scope: "transaction",
11
13
  }),
12
14
  executeSql: createPrismaSqlStatementExecutor(transactional),
@@ -1,3 +1,4 @@
1
+ import type { AvConfigComputedFields } from "@aventara/core";
1
2
  import { type AvPrismaAdapterModel, type AvPrismaReferentialActionTable, type PrismaClientShape, type PrismaTranslationTable } from "../discovery/discovery-artifact.interface.js";
2
3
  import type { AvPrismaProvider } from "../provider/provider-profile.js";
3
4
  export type PrismaArtifactEmission = {
@@ -10,5 +11,10 @@ export type PrismaArtifactEmission = {
10
11
  readonly referentialActions: AvPrismaReferentialActionTable;
11
12
  /** The generated client's models and field references — beside `model`, outside the hash. */
12
13
  readonly client: PrismaClientShape;
14
+ /**
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.
17
+ */
18
+ readonly computedFields: AvConfigComputedFields | null;
13
19
  };
14
20
  export declare function emitDiscoveryArtifactModule(emission: PrismaArtifactEmission): Promise<string>;
@@ -50,21 +50,74 @@ const MODEL_DOC = [
50
50
  " * one is answered `A2004`/`V1000` at its path before the database is reached.",
51
51
  " */",
52
52
  ];
53
+ const MODEL_TYPE_DOC = [
54
+ "/**",
55
+ " * The model's type, by name: a declaration that holds the model (an exported",
56
+ " * configuration, an `AppFramework` alias) writes `AvModel` rather",
57
+ " * than the whole model. `typeof model` is this type.",
58
+ " */",
59
+ ];
60
+ const COMPUTED_FIELDS_DOC = [
61
+ "/**",
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",
65
+ " * create's computed fields out; every other required field stays required.",
66
+ " * The framework warns at startup when the configuration says otherwise: run",
67
+ " * `aventara:prepare` again. Not part of `modelHash`.",
68
+ " */",
69
+ ];
70
+ const NO_COMPUTED_FIELDS_DOC = [
71
+ "/**",
72
+ " * `null`: the generate step was given no configuration (`--config`), so the",
73
+ " * stages before write compute may leave any create field out. Not part of",
74
+ " * `modelHash`.",
75
+ " */",
76
+ ];
53
77
  const DISCOVERY_DOC = [
54
78
  "/** Pass this to `createPrismaAdapter({ discovery })`. */",
55
79
  ];
56
80
  function canonicalLiteral(value) {
57
81
  return JSON.stringify(JSON.parse(canonicalizeJson(value)), null, "\t");
58
82
  }
83
+ function computedFieldsLines(computedFields) {
84
+ if (computedFields === null) {
85
+ return [
86
+ ...NO_COMPUTED_FIELDS_DOC,
87
+ "export const computedFields = null;",
88
+ "",
89
+ ];
90
+ }
91
+ return [
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;",
97
+ "",
98
+ ];
99
+ }
59
100
  export async function emitDiscoveryArtifactModule(emission) {
60
101
  const modelHash = await computeModelHash(emission.model);
61
102
  return `${[
62
103
  ...BANNER,
63
104
  "",
64
- 'import type { AvAdapterModel } from "@aventara/core";',
105
+ emission.computedFields === null
106
+ ? 'import type { AvAdapterModel } from "@aventara/core";'
107
+ : 'import type { AvAdapterModel, AvConfigComputedFields } from "@aventara/core";',
108
+ "",
109
+ `const discoveredModel = ${canonicalLiteral(emission.model)} as const satisfies AvAdapterModel;`,
110
+ "",
111
+ ...computedFieldsLines(emission.computedFields),
112
+ ...MODEL_TYPE_DOC,
113
+ "export type AvModel = {",
114
+ "\treadonly [K in keyof typeof discoveredModel]: (typeof discoveredModel)[K];",
115
+ emission.computedFields === null
116
+ ? "};"
117
+ : '} & { readonly "~computedFields"?: AvComputedFields };',
65
118
  "",
66
119
  ...MODEL_DOC,
67
- `export const model = ${canonicalLiteral(emission.model)} as const satisfies AvAdapterModel;`,
120
+ "export const model: AvModel = discoveredModel;",
68
121
  "",
69
122
  ...TRANSLATION_DOC,
70
123
  `export const translation = ${canonicalLiteral(emission.translation)} as const;`,
@@ -89,6 +142,7 @@ export async function emitDiscoveryArtifactModule(emission) {
89
142
  "\ttranslation,",
90
143
  "\treferentialActions,",
91
144
  "\tclient,",
145
+ "\tcomputedFields,",
92
146
  "} as const;",
93
147
  ].join("\n")}\n`;
94
148
  }
@@ -0,0 +1,19 @@
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;