@loradb/lora-graphql 0.24.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +151 -31
  2. package/dist/analyze/input-schemas.d.ts +11 -0
  3. package/dist/analyze/plans.d.ts +8 -0
  4. package/dist/analyze/stored.d.ts +29 -0
  5. package/dist/cli.js +263 -244
  6. package/dist/cli.js.map +1 -1
  7. package/dist/compile/cache.d.ts +9 -1
  8. package/dist/compile/context.d.ts +3 -0
  9. package/dist/compile/read/common.d.ts +2 -0
  10. package/dist/compile/read/root.d.ts +7 -0
  11. package/dist/compile/selection.d.ts +5 -0
  12. package/dist/diff-CdZZG-N1.js +134 -0
  13. package/dist/diff-CdZZG-N1.js.map +1 -0
  14. package/dist/driver.d.ts +36 -1
  15. package/dist/errors.d.ts +21 -1
  16. package/dist/events/outbox.d.ts +14 -0
  17. package/dist/events/worker.d.ts +67 -0
  18. package/dist/execute/mutate/checks.d.ts +16 -0
  19. package/dist/execute/mutate/env.d.ts +50 -0
  20. package/dist/execute/mutate/errors.d.ts +6 -0
  21. package/dist/execute/mutate/links.d.ts +38 -0
  22. package/dist/execute/mutate/plan.d.ts +5 -0
  23. package/dist/execute/mutate/runner.d.ts +101 -25
  24. package/dist/execute/mutate/single-call.d.ts +92 -0
  25. package/dist/execute/mutate/triggers.d.ts +21 -0
  26. package/dist/execute/mutate/unique-together.d.ts +5 -1
  27. package/dist/execute/mutate/validate.d.ts +11 -0
  28. package/dist/execute/transaction.d.ts +13 -2
  29. package/dist/index.d.ts +7 -2
  30. package/dist/index.js +16 -15
  31. package/dist/lora-graphql-D11QFUkf.js +17222 -0
  32. package/dist/lora-graphql-D11QFUkf.js.map +1 -0
  33. package/dist/lora-graphql.d.ts +30 -1
  34. package/dist/model/build/authorization.d.ts +11 -1
  35. package/dist/model/build/constraints.d.ts +22 -0
  36. package/dist/model/build/rule-checks.d.ts +6 -1
  37. package/dist/model/build/trigger.d.ts +9 -0
  38. package/dist/model/constraints.d.ts +112 -0
  39. package/dist/model/desugar.d.ts +7 -0
  40. package/dist/model/directives.d.ts +1 -1
  41. package/dist/model/types.d.ts +34 -0
  42. package/dist/options.d.ts +76 -3
  43. package/dist/results.d.ts +12 -1
  44. package/dist/schema/scalars.d.ts +8 -1
  45. package/dist/standard-schema.d.ts +37 -0
  46. package/dist/testing.js +1 -1
  47. package/package.json +2 -2
  48. package/dist/diff-K8-GSeKH.js +0 -100
  49. package/dist/diff-K8-GSeKH.js.map +0 -1
  50. package/dist/driver-CXKPLfkz.js +0 -14447
  51. package/dist/driver-CXKPLfkz.js.map +0 -1
package/dist/driver.d.ts CHANGED
@@ -36,10 +36,22 @@ export interface RunOptions {
36
36
  * only stored properties. Cheap enough to run synchronously.
37
37
  */
38
38
  bounded?: boolean;
39
+ /**
40
+ * A mutation sent as one write batch: counted against
41
+ * `maxQueuedWrites`, as a transaction waiting for the lock is.
42
+ */
43
+ queued?: boolean;
39
44
  }
40
45
  /** An open interactive transaction: statements see earlier writes. */
41
46
  export interface DriverTransaction {
42
47
  execute(statement: Statement): Promise<QueryResult>;
48
+ /**
49
+ * Run several statements in one call, in order, each seeing the writes
50
+ * of those before it. The first failure rolls the transaction back and
51
+ * rejects with that statement's error. Optional: without it the
52
+ * statements run one `execute()` at a time.
53
+ */
54
+ executeMany?(statements: Statement[]): Promise<QueryResult[]>;
43
55
  commit(): Promise<void>;
44
56
  rollback(): Promise<void>;
45
57
  /** False once committed, rolled back, or failed (a failure rolls back). */
@@ -75,6 +87,12 @@ export interface DriverChange {
75
87
  startId?: number;
76
88
  endId?: number;
77
89
  properties?: Record<string, unknown>;
90
+ /** For an update: the properties the transaction set and removed. */
91
+ setKeys?: string[];
92
+ removedKeys?: string[];
93
+ /** For a node update: the labels it added and removed. */
94
+ addedLabels?: string[];
95
+ removedLabels?: string[];
78
96
  }
79
97
  export interface DriverChangeBatch {
80
98
  lsn: number;
@@ -107,11 +125,21 @@ export interface LoraDatabaseLike {
107
125
  }): AsyncIterable<DriverChangeBatch> & {
108
126
  ready: Promise<void>;
109
127
  };
110
- begin?(mode?: "read_write" | "read_only"): Promise<{
128
+ begin?(mode?: "read_write" | "read_only", options?: {
129
+ timeoutMs?: number;
130
+ signal?: AbortSignal;
131
+ }): Promise<{
111
132
  execute(query: string, params?: never, options?: {
112
133
  timeoutMs?: number;
113
134
  signal?: AbortSignal;
114
135
  }): Promise<QueryResult>;
136
+ executeMany?(statements: Array<{
137
+ query: string;
138
+ params?: never;
139
+ }>, options?: {
140
+ timeoutMs?: number;
141
+ signal?: AbortSignal;
142
+ }): Promise<QueryResult[]>;
115
143
  commit(): Promise<void>;
116
144
  rollback(): Promise<void>;
117
145
  readonly isOpen: boolean;
@@ -126,3 +154,10 @@ export interface LoraDatabaseLike {
126
154
  * ```
127
155
  */
128
156
  export declare function loraDriver(db: LoraDatabaseLike): LoraDriver;
157
+ /**
158
+ * `driver`, refusing a write transaction when `max` are already waiting
159
+ * for the writer lock: one writer runs at a time, so past saturation a
160
+ * queue only adds latency. `onFull` builds the error to throw. Reads are
161
+ * not counted: they do not wait for the lock.
162
+ */
163
+ export declare function boundedWrites(driver: LoraDriver, max: number, onFull: (waiting: number) => Error): LoraDriver;
package/dist/errors.d.ts CHANGED
@@ -20,7 +20,7 @@ export declare function formatProblem(p: ModelProblem): string;
20
20
  * a stable order. Use it to exhaustively map codes (HTTP status, client
21
21
  * copy) or to test membership at runtime.
22
22
  */
23
- export declare const LORA_GRAPHQL_ERROR_CODES: readonly ["BAD_USER_INPUT", "INVALID_CURSOR", "LIMIT_EXCEEDED", "COST_EXCEEDED", "TIMEOUT", "UNAUTHENTICATED", "FORBIDDEN", "NOT_FOUND", "CONSTRAINT_VIOLATION", "DATABASE_ERROR", "PERSISTED_QUERY_ONLY", "WRONG_OPERATION_TYPE"];
23
+ export declare const LORA_GRAPHQL_ERROR_CODES: readonly ["BAD_USER_INPUT", "INVALID_CURSOR", "LIMIT_EXCEEDED", "COST_EXCEEDED", "OVERLOADED", "TIMEOUT", "UNAUTHENTICATED", "FORBIDDEN", "NOT_FOUND", "CONSTRAINT_VIOLATION", "DATABASE_ERROR", "PERSISTED_QUERY_ONLY", "WRONG_OPERATION_TYPE"];
24
24
  export type LoraGraphQLErrorCode = (typeof LORA_GRAPHQL_ERROR_CODES)[number];
25
25
  /**
26
26
  * Whether `err` is a request-time error raised by this library: a
@@ -37,6 +37,26 @@ export declare function isLoraGraphQLError(err: unknown): err is {
37
37
  };
38
38
  /** A request-time error with a stable `extensions.code`. */
39
39
  export declare function requestError(code: LoraGraphQLErrorCode, message: string, cause?: unknown, extensions?: Record<string, unknown>): GraphQLError;
40
+ /** One value a mutation was given, or computed, that breaks a field's rule. */
41
+ export interface Violation {
42
+ /** Where the value is in the mutation's arguments: keys and list indices. */
43
+ path: Array<string | number>;
44
+ /** The model field whose rule it is, as `Type.field`. */
45
+ coordinate: string;
46
+ /** The rule, named as its JSON Schema keyword (`maxLength`, `format`, ...). */
47
+ rule: string;
48
+ /** The rule's own numbers and names, enough to localise the message. */
49
+ params: Record<string, unknown>;
50
+ message: string;
51
+ }
52
+ /** The most violations one error lists; a batch can hold thousands. */
53
+ export declare const MAX_VIOLATIONS = 100;
54
+ /**
55
+ * Every value a mutation got wrong, in one error: `extensions.violations`
56
+ * lists each with its path, rule and parameters. The values themselves
57
+ * are never included: a rule is often on a password or an address.
58
+ */
59
+ export declare function validationError(code: "BAD_USER_INPUT" | "CONSTRAINT_VIOLATION", violations: readonly Violation[], total?: number): GraphQLError;
40
60
  /**
41
61
  * One error per field path pattern: errors with the same message and code
42
62
  * at paths that differ only in list indices (a field every row of a list
@@ -0,0 +1,14 @@
1
+ export declare const OUTBOX: {
2
+ /** `PENDING`, `CLAIMED`, `DONE` or `FAILED`. */
3
+ readonly state: "_outbox";
4
+ /** Handler calls that failed so far. */
5
+ readonly attempts: "_outboxAttempts";
6
+ /** Not before this time (epoch ms): the backoff after a failure. */
7
+ readonly at: "_outboxAt";
8
+ /** A claim is void past this time (epoch ms): its worker died. */
9
+ readonly lease: "_outboxLease";
10
+ /** The message of the last failure. */
11
+ readonly error: "_outboxError";
12
+ };
13
+ /** Whether `property` is one of the delivery-state properties. */
14
+ export declare function isOutboxProperty(property: string): boolean;
@@ -0,0 +1,67 @@
1
+ import { LoraDriver } from '../driver.js';
2
+ import { WriteChange } from '../execute/changes.js';
3
+ import { NodeType } from '../model/types.js';
4
+ import { DatabaseErrorEvent } from '../options.js';
5
+ export interface WorkerOptions {
6
+ /**
7
+ * Called with the node's stored scalar fields, by field name, read
8
+ * without read rules. Resolving marks the node done; throwing (or
9
+ * rejecting) schedules it again, up to `maxAttempts`.
10
+ */
11
+ handle: (node: Record<string, unknown>, info: {
12
+ /** The node's @key value: the idempotency key of the work. */
13
+ key: unknown;
14
+ /** 1 for the first call, 2 for the first retry, ... */
15
+ attempt: number;
16
+ /** Aborted when the worker stops. */
17
+ signal: AbortSignal;
18
+ }) => unknown;
19
+ /** Handlers running at once. Default 4. */
20
+ concurrency?: number;
21
+ /**
22
+ * Calls before a node is given up as `FAILED` and reported to
23
+ * `onError`. A call that never returned (its worker died, or it
24
+ * outlived `leaseMs`) counts. Default 8.
25
+ */
26
+ maxAttempts?: number;
27
+ /**
28
+ * Milliseconds to wait before attempt `attempt + 1`. Default: one
29
+ * second doubling per attempt, at most five minutes.
30
+ */
31
+ backoffMs?: (attempt: number) => number;
32
+ /**
33
+ * How often the worker looks for pending nodes it was not told about:
34
+ * work left by a restart, retries that are due, claims whose worker
35
+ * died. Milliseconds, default 30 000.
36
+ */
37
+ sweepMs?: number;
38
+ /**
39
+ * How long a claim holds, in milliseconds. Past it another worker may
40
+ * take the node, and what the first handler then returns is ignored:
41
+ * give a handler a timeout shorter than this. Default 60 000.
42
+ */
43
+ leaseMs?: number;
44
+ }
45
+ export interface Worker {
46
+ /**
47
+ * Resolves once nothing is pending: after a look at the database, with
48
+ * no handler running and no node due. For tests and for shutdown.
49
+ */
50
+ idle(): Promise<void>;
51
+ /** Stop taking work; resolves when the running handlers have ended. */
52
+ stop(): Promise<void>;
53
+ }
54
+ export interface WorkerHost {
55
+ driver: LoraDriver;
56
+ node: NodeType;
57
+ timeoutMs: number;
58
+ /** Committed writes, from the change hub. */
59
+ listen: (listener: (change: WriteChange) => void) => () => unknown;
60
+ onError: ((event: DatabaseErrorEvent) => void) | undefined;
61
+ }
62
+ export declare class OutboxWorker implements Worker {
63
+ #private;
64
+ constructor(host: WorkerHost, options: WorkerOptions);
65
+ idle(): Promise<void>;
66
+ stop(): Promise<void>;
67
+ }
@@ -1,3 +1,4 @@
1
+ import { NodeType } from '../../model/types.js';
1
2
  import { WritePlan } from './plan.js';
2
3
  import { Runner } from './runner.js';
3
4
  /**
@@ -5,6 +6,12 @@ import { Runner } from './runner.js';
5
6
  * the relationship was created from. Check every node that gained one.
6
7
  */
7
8
  export declare function checkCardinality(runner: Runner, plan: WritePlan): Promise<void>;
9
+ /**
10
+ * {@link checkCardinality} for a single call: each check is a statement
11
+ * that fails on the first node holding more than one, naming the node and
12
+ * its counts in the message. Nothing is awaited.
13
+ */
14
+ export declare function assertCardinality(runner: Runner, plan: WritePlan): void;
8
15
  /**
9
16
  * `@uniqueTogether`: after every write of the mutation, no two nodes of
10
17
  * a constrained type share a combination. Checks the nodes it created
@@ -14,6 +21,15 @@ export declare function checkCardinality(runner: Runner, plan: WritePlan): Promi
14
21
  * rule, so the bypass does not skip it.
15
22
  */
16
23
  export declare function checkUniqueTogether(runner: Runner): Promise<void>;
24
+ /**
25
+ * `@uniqueTogether` for a single call, on the nodes with `keys` of each
26
+ * type: every check is issued before this first waits, as a statement
27
+ * that fails on the node sharing its combination.
28
+ */
29
+ export declare function assertUniqueTogether(runner: Runner, touched: Array<{
30
+ node: NodeType;
31
+ keys: unknown[];
32
+ }>): Promise<void>;
17
33
  /**
18
34
  * A required single relationship stays set: disconnecting from the other
19
35
  * side must not leave the owner without one.
@@ -1,8 +1,10 @@
1
+ import { TypeValidator } from '../../standard-schema.js';
1
2
  import { Statistics } from '../../analyze/statistics.js';
2
3
  import { SelectionContext } from '../../compile/selection.js';
3
4
  import { DriverTransaction, LoraDriver, QueryResult, Statement } from '../../driver.js';
4
5
  import { GraphModel, NodeType } from '../../model/types.js';
5
6
  import { WriteChange } from '../changes.js';
7
+ import { LoraTransaction } from '../transaction.js';
6
8
  /** Computes a `@populatedBy` field. */
7
9
  export type PopulatedByCallback = (args: {
8
10
  operation: "CREATE" | "UPDATE";
@@ -15,6 +17,33 @@ export type PopulatedByCallback = (args: {
15
17
  /** The GraphQL context. */
16
18
  context: unknown;
17
19
  }) => unknown;
20
+ /** What a JavaScript trigger is called with. */
21
+ export interface TriggerEvent {
22
+ operation: "CREATE" | "UPDATE" | "DELETE";
23
+ /** The node type whose nodes were written. */
24
+ type: string;
25
+ /** Their @key values. For DELETE the nodes still exist. */
26
+ keys: unknown[];
27
+ /** The mutation's write-set so far. */
28
+ change: WriteChange;
29
+ /**
30
+ * The mutation's transaction: `execute()` runs Cypher in it, and
31
+ * `lora.execute({ context: { ...context, transaction } })` runs a
32
+ * generated mutation in it, whose writes join the write-set. The
33
+ * mutation commits it; throw to roll everything back.
34
+ */
35
+ transaction: LoraTransaction;
36
+ /** The GraphQL context of the request. */
37
+ context: unknown;
38
+ }
39
+ /** A trigger function; it may return a promise. Throwing vetoes the write. */
40
+ export type TriggerFunction = (event: TriggerEvent) => unknown;
41
+ /** The trigger functions of one node type. */
42
+ export interface TriggerFunctions {
43
+ create?: TriggerFunction;
44
+ update?: TriggerFunction;
45
+ delete?: TriggerFunction;
46
+ }
18
47
  export interface MutationEnv {
19
48
  model: GraphModel;
20
49
  driver: LoraDriver;
@@ -43,6 +72,27 @@ export interface MutationEnv {
43
72
  /** Statistics from `analyze()`, for filter costs. */
44
73
  statistics?: Statistics | undefined;
45
74
  callbacks: Readonly<Record<string, PopulatedByCallback>>;
75
+ /** The `singleCallMutations` option; false keeps every mutation in a transaction. */
76
+ singleCall?: boolean | undefined;
77
+ /** The `triggers` option: functions per node type and operation. */
78
+ triggers?: Readonly<Record<string, TriggerFunctions>> | undefined;
79
+ /** The `triggerTimeoutMs` option, defaulted. */
80
+ triggerTimeoutMs?: number | undefined;
81
+ /**
82
+ * The mutation runs inside a trigger's transaction: it fires no trigger
83
+ * itself, so what a trigger writes never cascades.
84
+ */
85
+ inTrigger?: boolean | undefined;
86
+ /**
87
+ * `@populatedBy` callbacks already started, by the input they compute
88
+ * for and the field's name: those of the mutation's own inputs start
89
+ * before the transaction opens, all at once, so they do not run under
90
+ * the writer lock one after another. Each is awaited where it would
91
+ * have been called, so a failure surfaces at the same point.
92
+ */
93
+ computed?: WeakMap<object, Map<string, Promise<unknown>>> | undefined;
94
+ /** The `validators` option: a Standard Schema per node type. */
95
+ validators?: Readonly<Record<string, TypeValidator>> | undefined;
46
96
  /**
47
97
  * A caller-owned transaction: run inside it and leave the commit to the
48
98
  * caller. A failed mutation rolls it back, so it is never half-applied.
@@ -1,5 +1,11 @@
1
1
  import { GraphModel, NodeType, RelationshipField } from '../../model/types.js';
2
2
  export declare function notConnected(owner: NodeType, rel: RelationshipField, target: NodeType, to: unknown): import('graphql').GraphQLError;
3
+ /** The error of a connect whose targets are not there (or not visible). */
4
+ export declare function noTarget(owner: NodeType, rel: RelationshipField, target: NodeType, missing: unknown[]): import('graphql').GraphQLError;
5
+ /** The error of a relationship field's rules refusing `op`. */
6
+ export declare function refusedRule(op: "CONNECT" | "DISCONNECT" | "UPDATE_EDGE", rel: RelationshipField): import('graphql').GraphQLError;
7
+ /** The error of a single relationship field that would hold `count` nodes. */
8
+ export declare function tooMany(field: RelationshipField, key: unknown, count: number, seen: number): import('graphql').GraphQLError;
3
9
  /** In place of the key of a node the caller cannot read, in messages. */
4
10
  export declare const HIDDEN: unique symbol;
5
11
  export declare function requiredMissing(owner: NodeType, field: RelationshipField, target: string, key: unknown): import('graphql').GraphQLError;
@@ -0,0 +1,38 @@
1
+ import { NodeType, RelationshipField } from '../../model/types.js';
2
+ import { WritePlan } from './plan.js';
3
+ import { Runner } from './runner.js';
4
+ /**
5
+ * The links of a single call: what {@link applyLinks} does, with
6
+ * nothing awaited. The rules on both ends and on the relationship field
7
+ * are assertions, and so is every target being there: the link reports
8
+ * the rows it connected in the message of the assertion that fails
9
+ * when one is missing.
10
+ */
11
+ export declare function linkInOne(runner: Runner, plan: WritePlan): void;
12
+ /**
13
+ * Connect `plan`'s links in a transaction, a relationship field at a
14
+ * time: the rules on both ends, the MERGE, then the rules on what it
15
+ * created or found.
16
+ */
17
+ export declare function applyLinks(runner: Runner, plan: WritePlan): Promise<void>;
18
+ /**
19
+ * Rules on the relationship field (and on a field declaring the same
20
+ * relationship from the other side) for `op`, on each pair's
21
+ * relationship as it stands: after the write for CONNECT and
22
+ * UPDATE_EDGE, before it for DISCONNECT. FORBIDDEN when any fails; the
23
+ * mutation rolls back.
24
+ */
25
+ export declare function checkRelationshipRules(runner: Runner, op: "CONNECT" | "DISCONNECT" | "UPDATE_EDGE", rel: RelationshipField, pairs: Array<{
26
+ from: unknown;
27
+ to: unknown;
28
+ }>): Promise<void>;
29
+ /** Whether either end has validate rules for `op` at `when`. */
30
+ export declare function endRules(owner: NodeType, target: NodeType, op: "CREATE_RELATIONSHIP" | "DELETE_RELATIONSHIP", when: "BEFORE" | "AFTER"): boolean;
31
+ /**
32
+ * Validate rules for connecting or disconnecting, on both ends of each
33
+ * pair: FORBIDDEN when either end fails them.
34
+ */
35
+ export declare function checkEnds(runner: Runner, op: "CREATE_RELATIONSHIP" | "DELETE_RELATIONSHIP", owner: NodeType, target: NodeType, pairs: Array<{
36
+ from: unknown;
37
+ to: unknown;
38
+ }>, when: "BEFORE" | "AFTER"): Promise<void>;
@@ -74,6 +74,11 @@ export declare class WritePlan {
74
74
  }>;
75
75
  /** `@populatedBy` callbacks for planned creates. */
76
76
  pending: Array<() => Promise<void>>;
77
+ /**
78
+ * The plan is an update's: a link may find its relationship there
79
+ * already, which a single call must then tell in the database.
80
+ */
81
+ reconnects: boolean;
77
82
  constructor(env: MutationEnv, ctx: CompileContext);
78
83
  get model(): GraphModel;
79
84
  /**
@@ -1,11 +1,13 @@
1
1
  import { SelectionSetNode } from 'graphql';
2
2
  import { CompileContext } from '../../compile/context.js';
3
- import { DriverTransaction } from '../../driver.js';
4
- import { NodeType, RelationshipField, ScalarField } from '../../model/types.js';
3
+ import { DriverTransaction, QueryResult, Statement } from '../../driver.js';
4
+ import { NodeType, ScalarField } from '../../model/types.js';
5
5
  import { RelationshipRef, WriteChange } from '../changes.js';
6
6
  import { MutationEnv, MutationInfo } from './env.js';
7
+ import { Assertions } from './single-call.js';
7
8
  import { WritePlan, Input } from './plan.js';
8
9
  export declare class Runner {
10
+ #private;
9
11
  readonly env: MutationEnv;
10
12
  readonly tx: DriverTransaction;
11
13
  readonly change: WriteChange;
@@ -17,6 +19,12 @@ export declare class Runner {
17
19
  keys: unknown[];
18
20
  fields: Set<string>;
19
21
  }>;
22
+ /** Whether a trigger function was called: it may have closed the transaction. */
23
+ triggerFunctionRan: boolean;
24
+ /** Nodes a trigger added to the write-set: they fire no trigger. */
25
+ triggered: Set<string>;
26
+ /** The fields each update set, per type and key: `@trigger(fields:)`. */
27
+ updatedFields: Map<NodeType, Map<string, Set<string>>>;
20
28
  /**
21
29
  * A type this mutation creates under a key held by a node the caller
22
30
  * cannot read; see {@link Runner.moveHiddenKeys}.
@@ -30,12 +38,100 @@ export declare class Runner {
30
38
  node: NodeType;
31
39
  field: ScalarField;
32
40
  } | undefined;
41
+ /** What the assertions of a single call stand for. */
42
+ readonly assertions: Assertions;
33
43
  constructor(env: MutationEnv, tx: DriverTransaction, change: WriteChange);
44
+ /** Whether the mutation is one call that commits as it returns. */
45
+ get single(): boolean;
46
+ /** The mutation needs more than one round trip: open its transaction. */
47
+ open(): Promise<void>;
48
+ /**
49
+ * In a single call, what JavaScript would decide after the mutation's
50
+ * statements is their last statement instead: an assertion that no node
51
+ * sat under a key or a unique value this create moved aside. It is last,
52
+ * so any other error is still the one a free key gets.
53
+ */
54
+ assertNothingMoved(): void;
55
+ /**
56
+ * Put a statement together and issue it. A single call puts every
57
+ * statement together before the first is sent, where a transaction
58
+ * does so once the one before it answered. An error raised meanwhile (a
59
+ * rule the claims settle) must not outrank what an earlier statement
60
+ * raises in the database: it becomes a statement that fails with it, in
61
+ * its place. Before the links' first statement it is thrown, as a
62
+ * transaction throws it: nothing was sent yet there either.
63
+ */
64
+ inPlace(issue: () => void): void;
65
+ /**
66
+ * Run `statement` in the transaction. Statements issued in the same
67
+ * tick, without awaiting one before issuing the next, go to the
68
+ * database in one call (`executeMany`), in the order issued: one round
69
+ * trip under the writer lock instead of one each. When a statement of
70
+ * such a batch fails, every statement of the batch rejects with its
71
+ * error, so a batch holds writes whose results are not checked,
72
+ * followed by reads.
73
+ */
74
+ send(statement: Statement): Promise<QueryResult>;
75
+ /**
76
+ * Issue a write whose result nothing checks, and leave it for the next
77
+ * statements to share its round trip: it is sent with whatever is
78
+ * issued before the event loop turns. `settle()` awaits it.
79
+ */
80
+ later(statement: Statement): void;
81
+ /**
82
+ * Leave `pending` (a statement issued, with what follows from its
83
+ * rows) for `settle()` to await.
84
+ */
85
+ defer(pending: Promise<unknown>): void;
86
+ /** The links of a single call begin; see {@link Runner.inPlace}. */
87
+ linking(): void;
88
+ /** Wait for the writes issued with `later`; throws the first failure. */
89
+ settle(): Promise<void>;
90
+ /**
91
+ * `pending`, safe to leave unawaited, and the mutation abandoned the
92
+ * moment it fails: before a batch still waiting to be sent goes out.
93
+ */
94
+ watch<T>(pending: Promise<T>): Promise<T>;
95
+ /**
96
+ * The mutation failed: nothing queued is sent. A single call commits
97
+ * with its first batch, so a batch must not go out after the error that
98
+ * should have stopped it.
99
+ */
100
+ abandon(err: unknown): void;
34
101
  ctx(): CompileContext;
102
+ /**
103
+ * An update of one node by key as one statement, for a single call:
104
+ * the match under the caller's filter rules, the BEFORE rules, the
105
+ * write and the AFTER rules, each rule an assertion. Which nodes it
106
+ * touched is then decided in the database, not between two round
107
+ * trips. Returns nothing when the update is not that simple (it writes
108
+ * relationships, adjusts a field with a constraint, or cannot be
109
+ * prepared without knowing whether the node is there): the caller opens
110
+ * a transaction instead.
111
+ */
112
+ updateInOne(plan: WritePlan, node: NodeType, key: unknown, input: Input, adjust: Input | undefined, tail: () => void): Promise<{
113
+ done: Promise<unknown[]>;
114
+ } | undefined>;
115
+ /**
116
+ * The single call found no node to update and rolled back: nothing of
117
+ * what was planned happened.
118
+ */
119
+ nothingWritten(): void;
120
+ /**
121
+ * `send()` for a statement's rows. The promise is safe to leave
122
+ * unawaited: a batch fails as a whole, so when one statement of several
123
+ * issued together is refused, the others reject too, and whoever
124
+ * stopped at the first must not leave the rest unhandled.
125
+ */
35
126
  run(text: string, ctx: CompileContext): Promise<Array<Record<string, unknown>>>;
36
127
  plan(): WritePlan;
37
128
  /** Every planned write, in order, with the checks that follow them. */
38
- apply(plan: WritePlan): Promise<void>;
129
+ /**
130
+ * `tail` is called once the plan's last statements are issued, without
131
+ * waiting for them: what it issues (the payload read, the caller's own
132
+ * checks) shares their round trip.
133
+ */
134
+ apply(plan: WritePlan, tail?: () => void): Promise<void>;
39
135
  /**
40
136
  * `maxBatch` over the whole mutation: nodes created and updated so far
41
137
  * plus those `plan` creates and updates (nested `update` entries
@@ -69,28 +165,6 @@ export declare class Runner {
69
165
  moveTakenUniques(plan: WritePlan): Promise<void>;
70
166
  applyCreates(plan: WritePlan): Promise<void>;
71
167
  applyDisconnects(plan: WritePlan): Promise<void>;
72
- applyLinks(plan: WritePlan): Promise<void>;
73
- /**
74
- * Rules on the relationship field (and on a field declaring the same
75
- * relationship from the other side) for `op`, on each pair's
76
- * relationship as it stands: after the write for CONNECT and
77
- * UPDATE_EDGE, before it for DISCONNECT. FORBIDDEN when any fails; the
78
- * mutation rolls back.
79
- */
80
- checkRelationshipRules(op: "CONNECT" | "DISCONNECT" | "UPDATE_EDGE", rel: RelationshipField, pairs: Array<{
81
- from: unknown;
82
- to: unknown;
83
- }>): Promise<void>;
84
- /** Whether either end has validate rules for `op` at `when`. */
85
- endRules(owner: NodeType, target: NodeType, op: "CREATE_RELATIONSHIP" | "DELETE_RELATIONSHIP", when: "BEFORE" | "AFTER"): boolean;
86
- /**
87
- * Validate rules for connecting or disconnecting, on both ends of each
88
- * pair: FORBIDDEN when either end fails them.
89
- */
90
- checkEnds(op: "CREATE_RELATIONSHIP" | "DELETE_RELATIONSHIP", owner: NodeType, target: NodeType, pairs: Array<{
91
- from: unknown;
92
- to: unknown;
93
- }>, when: "BEFORE" | "AFTER"): Promise<void>;
94
168
  /** `update: [{ key, edge }]`: properties of connected pairs, in place. */
95
169
  applyEdgeUpdates(plan: WritePlan): Promise<void>;
96
170
  /**
@@ -127,3 +201,5 @@ export declare class Runner {
127
201
  */
128
202
  resolveKeys(node: NodeType, where: Input, op: "UPDATE" | "DELETE", limit: number): Promise<unknown[]>;
129
203
  }
204
+ /** `pending`, safe to leave unawaited when an earlier check throws. */
205
+ export declare function held<T>(pending: Promise<T>): Promise<T>;
@@ -0,0 +1,92 @@
1
+ import { DriverTransaction, LoraDriver, QueryResult, RunOptions, Statement } from '../../driver.js';
2
+ import { GraphModel } from '../../model/types.js';
3
+ import { WritePlan } from './plan.js';
4
+ /**
5
+ * A transaction that is one `driver.run()` call until `open()` makes it a
6
+ * real one. In its single form the first batch is the whole mutation: it
7
+ * is committed when the call returns, so `commit()` has nothing left to
8
+ * do, and a failure has already rolled everything back.
9
+ *
10
+ * With `single` false it is never one call, only late: the transaction
11
+ * begins with its first statement, so what a mutation works out before
12
+ * that (its plan, its `@populatedBy` callbacks) does not hold the lock.
13
+ */
14
+ export declare class LazyTransaction implements DriverTransaction {
15
+ #private;
16
+ constructor(driver: LoraDriver, options: RunOptions, single?: boolean);
17
+ /**
18
+ * Whether the single call ran and committed: an error after that can
19
+ * no longer undo the write.
20
+ */
21
+ get committed(): boolean;
22
+ /** Whether the mutation is still one call. */
23
+ get single(): boolean;
24
+ /** The mutation needs more than one round trip: begin a transaction. */
25
+ open(): Promise<void>;
26
+ execute(statement: Statement): Promise<QueryResult>;
27
+ executeMany(statements: Statement[]): Promise<QueryResult[]>;
28
+ commit(): Promise<void>;
29
+ rollback(): Promise<void>;
30
+ get isOpen(): boolean;
31
+ }
32
+ /**
33
+ * `err` without the position the binding appends to the error of a
34
+ * failing statement of a batch (`(statement 2 of 3)`): callers see the
35
+ * error a lone statement gives. An error whose message is read-only (the
36
+ * DOMException of an aborted request) is passed on as it is.
37
+ */
38
+ export declare function withoutPosition(err: unknown): unknown;
39
+ /**
40
+ * Whether the database has `assert()`: asked once per driver. Without it
41
+ * every mutation keeps its transaction and decides in JavaScript.
42
+ */
43
+ export declare function supportsAssert(driver: LoraDriver): Promise<boolean>;
44
+ /**
45
+ * Whether reading any node can be refused by a READ validate rule: a
46
+ * payload read would then need a decision after the write, which a single
47
+ * call cannot make.
48
+ */
49
+ export declare function readCanBeRefused(model: GraphModel): boolean;
50
+ /**
51
+ * What the guard of a single-call update that writes relationships stands
52
+ * for: the node is not there (or not the caller's to update), so the call
53
+ * rolled back and the mutation answers null, as a transaction would after
54
+ * finding nothing.
55
+ */
56
+ export declare const NOT_THERE: unique symbol;
57
+ /**
58
+ * Whether `plan`'s links can go out in a single call: when each is
59
+ * known to create its relationship. A transaction asks the database
60
+ * whether the pair was connected already, and applies other rules to a
61
+ * re-connect; here it must be certain beforehand. It is when one end is
62
+ * a node this plan creates (it has no relationships yet) and no two
63
+ * links name the same relationship (one from each side's field). An
64
+ * update's links ask the database instead (`plan.reconnects`). A
65
+ * property the rules refuse depends on what the link found, in
66
+ * JavaScript.
67
+ */
68
+ export declare function linksInOne(plan: WritePlan): boolean;
69
+ /** The message of an `assert()` that stands for a refused rule. */
70
+ export declare function refusal(kind: "forbidden" | "hidden" | "taken", type: string, detail: string): string;
71
+ /**
72
+ * The request error an assertion stood for, or `err` when it is not one
73
+ * of ours.
74
+ */
75
+ export declare function refused(model: GraphModel, err: unknown): unknown;
76
+ /**
77
+ * The assertions of one single call that stand for an error only this
78
+ * mutation can name (the row a check failed on, a target that is not
79
+ * there): each is numbered, and its number is its message.
80
+ */
81
+ export declare class Assertions {
82
+ #private;
83
+ /**
84
+ * The message, a Cypher expression, of an assertion that stands for
85
+ * `error`: {@link Assertions.refusedBy} gives the error back. `detail`
86
+ * is an expression appended to it, for an error that names what the
87
+ * rows held.
88
+ */
89
+ failsAs(error: (detail: string) => unknown, detail?: string): string;
90
+ /** The error an assertion of {@link Assertions.failsAs} stood for, if `err` is one. */
91
+ refusedBy(err: unknown): unknown;
92
+ }
@@ -0,0 +1,21 @@
1
+ import { CompileContext } from '../../compile/context.js';
2
+ import { Statement } from '../../driver.js';
3
+ import { MutationOperation, NodeType, Trigger } from '../../model/types.js';
4
+ import { MutationEnv } from './env.js';
5
+ import { Runner } from './runner.js';
6
+ /** Whether the mutation may fire a trigger: a statement or a function. */
7
+ export declare function hasTriggers(env: MutationEnv): boolean;
8
+ /** Whether the mutation may call a trigger function. */
9
+ export declare function hasTriggerFunctions(env: MutationEnv): boolean;
10
+ /** The trigger statement for `keys`, with `this` bound to each node. */
11
+ export declare function triggerStatement(ctx: CompileContext, node: NodeType, trigger: Trigger, operation: MutationOperation, keys: unknown[]): Statement;
12
+ /**
13
+ * Run the CREATE and UPDATE triggers for what the mutation wrote so far.
14
+ * A node the mutation created and then updated fires CREATE only.
15
+ */
16
+ export declare function runWriteTriggers(runner: Runner): Promise<void>;
17
+ /** Run the DELETE triggers of `doomed`, while the nodes still exist. */
18
+ export declare function runDeleteTriggers(runner: Runner, doomed: ReadonlyArray<{
19
+ node: NodeType;
20
+ keys: unknown[];
21
+ }>): Promise<void>;
@@ -14,5 +14,9 @@ export declare function checkUniqueTogetherOf(env: MutationEnv, tx: DriverTransa
14
14
  * compares it with the nodes that share its first relationship end (or,
15
15
  * with scalars only, its first scalar's value) — never a scan of the type.
16
16
  * Without `keys`, every node of the type is compared (a scan).
17
+ *
18
+ * With `failing`, the statement fails instead of returning that node:
19
+ * `failing` gives the assertion's message, from an expression for the
20
+ * node's place in `keys`.
17
21
  */
18
- export declare function uniqueTogetherStatement(ctx: CompileContext, model: GraphModel, node: NodeType, u: UniqueTogether, keys: unknown[] | undefined): string;
22
+ export declare function uniqueTogetherStatement(ctx: CompileContext, model: GraphModel, node: NodeType, u: UniqueTogether, keys: unknown[] | undefined, failing?: (index: string) => string): string;
@@ -0,0 +1,11 @@
1
+ import { GraphModel, NodeType } from '../../model/types.js';
2
+ import { MutationKind } from '../../schema/mutations.js';
3
+ import { TypeValidator } from '../../standard-schema.js';
4
+ export type Validators = Readonly<Record<string, TypeValidator>>;
5
+ /**
6
+ * Refuse a mutation whose input breaks a field's rule or its type's
7
+ * validator, with every violation in one `BAD_USER_INPUT`. Values
8
+ * computed during the write (an `adjust`, a `@populatedBy` callback) are
9
+ * checked where they appear.
10
+ */
11
+ export declare function validateInput(model: GraphModel, validators: Validators, op: MutationKind, node: NodeType, args: Record<string, unknown>): Promise<void>;