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

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,57 @@
1
1
  # @aventara/prisma7-adapter
2
2
 
3
+ ## 1.0.0-rc.8
4
+
5
+ 1.0.0-rc.5, rc.6 and rc.7 were tagged but never published; 1.0.0-rc.8 contains everything listed under them and the changes below.
6
+
7
+ ### Patch Changes
8
+
9
+ - 1c8e4ee: The discovery artifact's referential actions say `referenceFields` where they said `references`: in
10
+ `referentialActions.<Resource>.owningEdges[]`, the fields of the Resource itself that hold the reference
11
+ (`@relation(fields: [...])`). The model's relation fields already use `references` for the target's fields
12
+ (`@relation(references: [...])`) and `referenceFields` for these, so the same word meant two different lists in one
13
+ file. The generated `discovery.artifact.ts` changes on the next `aventara:prepare` (it runs on install, build and
14
+ start), and only in those keys: `modelHash` is unchanged, since the referential actions are outside it, and so is the
15
+ prebuilt Contracts file `aventara-build` writes. Code that read `owningEdges[i].references` reads
16
+ `owningEdges[i].referenceFields`.
17
+ - Updated dependencies [1c8e4ee]
18
+ - @aventara/core@1.0.0-rc.8
19
+
20
+ ## 1.0.0-rc.7
21
+
22
+ 1.0.0-rc.5 and 1.0.0-rc.6 were tagged but never published; 1.0.0-rc.7 contains everything listed under them and the changes below.
23
+
24
+ ### Patch Changes
25
+
26
+ - Updated dependencies [f917c1c]
27
+ - @aventara/core@1.0.0-rc.7
28
+
29
+ ## 1.0.0-rc.6
30
+
31
+ 1.0.0-rc.5 was tagged but never published. 1.0.0-rc.6 contains everything listed under 1.0.0-rc.5 and the fix below.
32
+
33
+ ### Patch Changes
34
+
35
+ - Updated dependencies [1cf72e0]
36
+ - @aventara/core@1.0.0-rc.6
37
+
38
+ ## 1.0.0-rc.5
39
+
40
+ ### Patch Changes
41
+
42
+ - 4795ff1: Two additions for policy engines. A Framework without a policy, and every Contract a caller is served, are unchanged.
43
+
44
+ - A policy engine can check a create whose row reaches another Resource through a relation. Discovery now states, on the end of a relation that holds the reference, which of the target's fields its reference fields name, pair by pair (`Note.campaign`: `referenceFields: ["campaignId"]`, `references: ["id"]`). An engine reads them from the model it is built from (a Prisma discovery artifact's `model`) or from `framework.contracts.core`, and asks one `exists` about the row they name: "a player creates a note only in a campaign they play in". Only the adapter's model and the Core Contract carry them. The Application, Client and Agent Contracts, `/_contract`, the MCP tools and every ClientContract hash are unchanged. The discovery artifact's model hash, and so a prebuilt Contracts file's fingerprint, change: regenerate the artifact and rebuild the prebuilt file.
45
+ - `fieldsOf({ access: "write" })` also names the action that writes the record, `action: "create" | "update"` (an upsert's branches each their own, a nested record its directive's), so an engine can lock a field on update and still let a create write it. A read question names none. An engine that ignores it keeps working.
46
+
47
+ - 9e178ca: Fixes for applications that check requests with a policy engine. A Framework without a policy behaves as before.
48
+
49
+ - A cursor on a row the caller cannot read is answered exactly as a cursor on a row that does not exist, at the root and in a to-many relation, whatever values the caller sends beside the identifier. Before, the page started at the hidden row, so it told that the row exists and where its values sort. The Prisma adapter reads the cursor's row under the read scope first: one statement more for a cursor on a readable row, and only that read for one on a row the caller cannot read.
50
+ - A unique update with a nested write (`$create`, `$connect`, …) on a row the caller can read but not change answers `A4002`, as the same update without one does. It answered `A2003`. A row the caller cannot read still answers `A2003`.
51
+ - The `exists` questions a policy asks select the Resource's primary identifier, never another unique field such as a governed `email`.
52
+ - `policy.principal` may answer with a promise. A synchronous principal works as before; a rejection answers `A3000`, as a throw does.
53
+ - `AvConstructionError.message` names the first problem and how many more there are, for example `Aventara framework configuration validation failed: CONFIG_INVALID_TYPE at adapter.execute: adapter.execute must be a function (+2 more)`. `stage` and `diagnostics` are unchanged.
54
+
3
55
  ## 1.0.0-rc.4
4
56
 
5
57
  ### Patch Changes
package/README.md CHANGED
@@ -529,10 +529,10 @@ discovery.referentialActions;
529
529
  // {
530
530
  // Shop: { owningEdges: [] },
531
531
  // Item: { owningEdges: [{ field: "shop", target: "Shop",
532
- // cardinality: "one", references: ["shopId"],
532
+ // cardinality: "one", referenceFields: ["shopId"],
533
533
  // onDelete: "cascade", onUpdate: "cascade" }] },
534
534
  // Clerk: { owningEdges: [{ field: "shop", target: "Shop",
535
- // cardinality: "one", references: ["shopId"],
535
+ // cardinality: "one", referenceFields: ["shopId"],
536
536
  // onDelete: "set-null", onUpdate: "cascade" }] },
537
537
  // }
538
538
  ```
@@ -544,7 +544,7 @@ read the enclosing Resource key: above, that is `Item` (cascade) and `Clerk` (se
544
544
  that sentence are Resource names and nothing in the shape tells them apart, so reading it backwards
545
545
  produces no error, only a plan that reasons backwards.
546
546
 
547
- `references` are the edge's `@relation(fields: […])`, the scalars that hold the reference. A record
547
+ `referenceFields` are the edge's `@relation(fields: […])`, the scalars that hold the reference (the relation field's `referenceFields` in the model; its `references` are the target's fields). A record
548
548
  that writes both a relation and one of its reference fields (`shopId` and `shop: { $connect: … }`,
549
549
  whether the relation is required or optional) is refused before the database is reached: `A2004` with
550
550
  a `V1012` issue at the relation, message `Set "shopId" or "shop", not both.` The reference fields are
@@ -668,7 +668,9 @@ Resource or operation for one to wrap.
668
668
  - A record written through a relation cannot write that relation, or its reference fields, back: Prisma's nested
669
669
  input for it leaves them out (a post created in `author.posts` has no `author` or `authorId`; a category created in
670
670
  a post's `category` has no `posts`). The artifact states each relation's other end (`inverse`, paired by relation
671
- name), the fields that hold its reference (`referenceFields`, from `@relation(fields: …)`) and no relation
671
+ name), the fields that hold its reference (`referenceFields`, from `@relation(fields: …)`) and the target's fields
672
+ they hold, pair by pair (`references`, from `@relation(references: …)`, which `@aventara/core` keeps in the Core
673
+ Contract only, for a policy engine), and no relation
672
674
  writable at the `"loopback"` position, so `@aventara/core` refuses such a write before any
673
675
  guard or SQL, `A2004` with a `V1012` issue at the field written back, and the types do not offer it. Where a layer
674
676
  hides the other end, Prisma's client refuses the write instead and the adapter answers that refusal the same way.
@@ -42,9 +42,11 @@ export type AvPrismaReferentialEdge = {
42
42
  readonly cardinality: "one" | "many";
43
43
  /**
44
44
  * The scalar fields of the enclosing resource that hold the reference: the
45
- * relation's `@relation(fields: […])`, in schema order.
45
+ * relation's `@relation(fields: […])`, in schema order. Named as the relation
46
+ * field's `referenceFields` in the model: its `references` are the TARGET's
47
+ * fields (`@relation(references: […])`).
46
48
  */
47
- readonly references: readonly string[];
49
+ readonly referenceFields: readonly string[];
48
50
  /** What deleting a `target` row does to rows of the enclosing resource. */
49
51
  readonly onDelete: AvPrismaReferentialAction;
50
52
  /** The same for an update of the referenced key. Read independently of `onDelete`. */
@@ -245,10 +245,17 @@ function composeResource(input) {
245
245
  const referenceFields = firstReference === undefined
246
246
  ? undefined
247
247
  : [firstReference, ...moreReferences];
248
+ const [firstTarget, ...moreTargets] = field.relation?.references ?? [];
249
+ const references = referenceFields === undefined ||
250
+ firstTarget === undefined ||
251
+ moreTargets.length + 1 !== referenceFields.length
252
+ ? undefined
253
+ : [firstTarget, ...moreTargets];
248
254
  fields[projection.name] = {
249
255
  ...projection.field,
250
256
  ...(inverse === undefined ? {} : { inverse: inverse.relation }),
251
257
  ...(referenceFields === undefined ? {} : { referenceFields }),
258
+ ...(references === undefined ? {} : { references }),
252
259
  capabilities: deriveRelationCapabilities({
253
260
  surface: relationSurface,
254
261
  }),
@@ -259,7 +266,7 @@ function composeResource(input) {
259
266
  field: projection.edge.field,
260
267
  target: projection.edge.target,
261
268
  cardinality: projection.edge.cardinality,
262
- references: [...(referenceFields ?? [])],
269
+ referenceFields: [...(referenceFields ?? [])],
263
270
  onDelete: projection.edge.onDelete,
264
271
  onUpdate: projection.edge.onUpdate,
265
272
  });
@@ -2,9 +2,10 @@ import { locateBackReferenceWrite, } from "../errors/back-reference.locator.js";
2
2
  import { locateUnboundBigint } from "../errors/bigint-binding.locator.js";
3
3
  import { classifyPrismaError, prismaNestedWriteFailure, prismaOutcomeFailure, prismaValueFailure, } from "../errors/error.normalizer.js";
4
4
  import { prismaDelegates, } from "../prisma-client.interface.js";
5
- import { PRISMA_VARIANT_DISPATCH, translateOperation, } from "../translation/operation.translator.js";
5
+ import { PRISMA_VARIANT_DISPATCH, translateOperation, translateScopedCursorLookup, } from "../translation/operation.translator.js";
6
6
  import { runFirstVariantEmulation } from "./first-variant.emulator.js";
7
7
  import { shapeCount, shapeRow, shapeRows, } from "./result.shaper.js";
8
+ import { resolveScopedCursors } from "./scoped-cursor.resolver.js";
8
9
  export class AvPrismaExecutionError extends Error {
9
10
  name = "AvPrismaExecutionError";
10
11
  }
@@ -21,8 +22,9 @@ export function createPrismaOperationExecutor(dependencies) {
21
22
  : {}),
22
23
  });
23
24
  return {
24
- async execute(operation) {
25
- const result = resultKindOf(dispatchOf(operation));
25
+ async execute(sent) {
26
+ const result = resultKindOf(dispatchOf(sent));
27
+ const operation = await withScopedCursorsResolved(dependencies, sent, (call) => issueClassified(issue, call, sent, locatorFor(sent)));
26
28
  const translation = translateOperation({
27
29
  model: dependencies.model,
28
30
  translation: dependencies.translation,
@@ -53,6 +55,18 @@ export function createPrismaOperationExecutor(dependencies) {
53
55
  },
54
56
  };
55
57
  }
58
+ async function withScopedCursorsResolved(dependencies, operation, issue) {
59
+ const args = operation.arguments;
60
+ const resolved = await resolveScopedCursors(dependencies.model, operation.resource, args, async (resource, cursor) => (await issue(translateScopedCursorLookup({
61
+ model: dependencies.model,
62
+ translation: dependencies.translation,
63
+ resource,
64
+ cursor,
65
+ }))) !== null);
66
+ return resolved === args
67
+ ? operation
68
+ : { ...operation, arguments: resolved };
69
+ }
56
70
  async function withUnit(dependencies, work) {
57
71
  if (dependencies.scope === "transaction") {
58
72
  return work(dependencies.client);
@@ -0,0 +1,11 @@
1
+ import type { AvAdapterModel } from "@aventara/core";
2
+ type Level = Readonly<Record<string, unknown>>;
3
+ /** Whether the row `cursor` (with its `AND`) names on `resource` meets its conditions. */
4
+ export type PrismaScopedCursorLookup = (resource: string, cursor: Level) => Promise<boolean>;
5
+ /**
6
+ * The operation's arguments with every cursor that carries `AND` resolved: its
7
+ * address alone when the row meets the conditions, otherwise no cursor and
8
+ * `limit: 0`. The same object when no cursor carries `AND`.
9
+ */
10
+ export declare function resolveScopedCursors(model: AvAdapterModel, resource: string, args: Level, lookup: PrismaScopedCursorLookup): Promise<Level>;
11
+ export {};
@@ -0,0 +1,47 @@
1
+ export async function resolveScopedCursors(model, resource, args, lookup) {
2
+ return (await resolveLevel(model, resource, args, lookup)) ?? args;
3
+ }
4
+ async function resolveLevel(model, resource, level, lookup) {
5
+ let resolved;
6
+ const cursor = level.cursor;
7
+ if (isRecord(cursor) && Object.hasOwn(cursor, "AND")) {
8
+ const { AND: _conditions, ...address } = cursor;
9
+ const { cursor: _cursor, ...rest } = level;
10
+ resolved = (await lookup(resource, cursor))
11
+ ? { ...rest, cursor: address }
12
+ : { ...rest, limit: 0 };
13
+ }
14
+ if (Array.isArray(level.select)) {
15
+ const select = await resolveSelect(model, resource, level.select, lookup);
16
+ if (select !== undefined) {
17
+ resolved = { ...(resolved ?? level), select };
18
+ }
19
+ }
20
+ return resolved;
21
+ }
22
+ async function resolveSelect(model, resource, select, lookup) {
23
+ let changed = false;
24
+ const entries = [];
25
+ for (const entry of select) {
26
+ if (!isRecord(entry)) {
27
+ entries.push(entry);
28
+ continue;
29
+ }
30
+ const rewritten = {};
31
+ for (const [name, value] of Object.entries(entry)) {
32
+ const field = model.resources[resource]?.fields[name];
33
+ const inner = field?.kind === "relation" && isRecord(value)
34
+ ? await resolveLevel(model, field.target, value, lookup)
35
+ : undefined;
36
+ if (inner !== undefined) {
37
+ changed = true;
38
+ }
39
+ rewritten[name] = inner ?? value;
40
+ }
41
+ entries.push(rewritten);
42
+ }
43
+ return changed ? entries : undefined;
44
+ }
45
+ function isRecord(value) {
46
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47
+ }
@@ -19,7 +19,7 @@ const REFERENTIAL_ACTIONS_DOC = [
19
19
  "/**",
20
20
  " * The schema's referential actions, per Resource: the relation fields that",
21
21
  " * declare `@relation(fields: …)`, each with the fields holding its reference",
22
- " * (`references`) and its resolved `onDelete` and `onUpdate`. An edge under",
22
+ " * (`referenceFields`) and its resolved `onDelete` and `onUpdate`. An edge under",
23
23
  " * Resource `R` reads:",
24
24
  " *",
25
25
  " * deleting a `target` row does `onDelete` to rows of `R`.",
@@ -12,7 +12,7 @@ function translateReferentialActions(table) {
12
12
  owningEdges.map((edge) => ({
13
13
  field: edge.field,
14
14
  target: edge.target,
15
- references: edge.references,
15
+ referenceFields: edge.referenceFields,
16
16
  onDelete: DELETE_EFFECTS[edge.onDelete],
17
17
  })),
18
18
  ]));
@@ -144,3 +144,14 @@ export type PrismaOperationTranslationInput = {
144
144
  * addresses no row — never a silently narrowed or widened call.
145
145
  */
146
146
  export declare function translateOperation(input: PrismaOperationTranslationInput): PrismaOperationTranslation;
147
+ /**
148
+ * The unique read that tells whether the row a core cursor names meets the
149
+ * `AND` conditions it carries: `findUnique` by the cursor's address with the
150
+ * conditions, translated against `resource`, in its selector.
151
+ */
152
+ export declare function translateScopedCursorLookup(input: {
153
+ readonly model: AvAdapterModel;
154
+ readonly translation: Readonly<Record<string, PrismaResourceTranslation>>;
155
+ readonly resource: string;
156
+ readonly cursor: Readonly<Record<string, unknown>>;
157
+ }): PrismaCall;
@@ -1,6 +1,6 @@
1
1
  import { addressIdentifier } from "./identifier.addressing.js";
2
2
  import { translateMutationData } from "./mutation.translator.js";
3
- import { translateOrderBy, translatePagination, } from "./order-pagination.translator.js";
3
+ import { translateCursorLookup, translateOrderBy, translatePagination, } from "./order-pagination.translator.js";
4
4
  import { translateProjection, } from "./projection.translator.js";
5
5
  import { translateWhere } from "./where.translator.js";
6
6
  const FIRST_VARIANT_WRITE_ROUTE = {
@@ -256,10 +256,39 @@ function paginationArguments(context) {
256
256
  ...(typeof context.args.offset === "number"
257
257
  ? { offset: context.args.offset }
258
258
  : {}),
259
- ...(isRecord(context.args.cursor) ? { cursor: context.args.cursor } : {}),
259
+ ...cursorArguments(context.args.cursor),
260
260
  identifiers: identifiersOf(context, context.resource),
261
261
  });
262
262
  }
263
+ function cursorArguments(cursor) {
264
+ if (!isRecord(cursor)) {
265
+ return {};
266
+ }
267
+ if (Object.hasOwn(cursor, "AND")) {
268
+ throw new AvPrismaOperationTranslationError("cursor: its `AND` conditions reach no Prisma `cursor`; the executor resolves them before translation");
269
+ }
270
+ return { cursor };
271
+ }
272
+ export function translateScopedCursorLookup(input) {
273
+ const { AND: conditions, ...address } = input.cursor;
274
+ const identifiers = input.translation[input.resource]?.identifierKeys;
275
+ if (identifiers === undefined) {
276
+ throw new AvPrismaOperationTranslationError(`Resource "${input.resource}" has no Prisma addressing in the translation table`);
277
+ }
278
+ return {
279
+ model: input.resource,
280
+ action: "findUnique",
281
+ args: translateCursorLookup({
282
+ cursor: address,
283
+ conditions: translateWhere({
284
+ model: input.model,
285
+ resource: input.resource,
286
+ where: { AND: conditions },
287
+ }),
288
+ identifiers,
289
+ }),
290
+ };
291
+ }
263
292
  function mutationData(context, stage, argument) {
264
293
  return translateMutationData({
265
294
  model: context.model,
@@ -322,7 +351,7 @@ function relationArgumentTranslator(context) {
322
351
  Object.assign(emitted, translatePagination({
323
352
  ...(typeof args.limit === "number" ? { limit: args.limit } : {}),
324
353
  ...(typeof args.offset === "number" ? { offset: args.offset } : {}),
325
- ...(isRecord(args.cursor) ? { cursor: args.cursor } : {}),
354
+ ...cursorArguments(args.cursor),
326
355
  identifiers: identifiersOf(context, target),
327
356
  }));
328
357
  return emitted;
@@ -30,4 +30,23 @@ export type OrderByTranslation = {
30
30
  readonly orderBy: unknown;
31
31
  };
32
32
  export declare function translatePagination(input: PaginationTranslation): PrismaPaginationArguments;
33
+ /**
34
+ * The Aventara cursor carries the active ordered scalar fields; core guarantees
35
+ * that they include a complete identifier (`V1009` — *"orderBy must include a
36
+ * complete Resource identifier as an explicit tie-breaker"*). Prisma's `cursor`
37
+ * is an ADDRESS, so the identifier is what is emitted: the remaining ordered
38
+ * fields are the ordering, which `orderBy` already carries, and adding them as
39
+ * equality members of a where-unique would narrow the address rather than
40
+ * sharpen it.
41
+ */
42
+ /**
43
+ * The unique read that tells whether the row a cursor names meets `conditions`
44
+ * (a translated `<M>WhereInput`): its address, the conditions in `AND`, and the
45
+ * address's members selected.
46
+ */
47
+ export declare function translateCursorLookup(input: {
48
+ readonly cursor: Readonly<Record<string, unknown>>;
49
+ readonly conditions: Readonly<Record<string, unknown>>;
50
+ readonly identifiers: readonly PrismaIdentifierAddress[];
51
+ }): Readonly<Record<string, unknown>>;
33
52
  export declare function translateOrderBy(input: OrderByTranslation): PrismaOrderByInput | readonly PrismaOrderByInput[];
@@ -24,6 +24,21 @@ export function translatePagination(input) {
24
24
  }
25
25
  return emitted;
26
26
  }
27
+ export function translateCursorLookup(input) {
28
+ const addressed = addressIdentifier({
29
+ addresses: input.identifiers,
30
+ values: input.cursor,
31
+ match: "subset",
32
+ emit: (_member, value) => value,
33
+ });
34
+ if (addressed === undefined) {
35
+ throw new AvPrismaOrderTranslationError(`cursor: no identifier is complete in { ${Object.keys(input.cursor).join(", ")} }, so it addresses no row`);
36
+ }
37
+ return {
38
+ where: { ...addressed.where, AND: [input.conditions] },
39
+ select: Object.fromEntries(addressed.address.members.map((member) => [member, true])),
40
+ };
41
+ }
27
42
  function addressCursor(cursor, identifiers) {
28
43
  const addressed = addressIdentifier({
29
44
  addresses: identifiers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/prisma7-adapter",
3
- "version": "1.0.0-rc.4",
3
+ "version": "1.0.0-rc.8",
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.4"
33
+ "@aventara/core": "1.0.0-rc.8"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@prisma/adapter-better-sqlite3": "^7.10.0",