@loradb/lora-graphql 0.21.0 → 0.22.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.
package/dist/driver.d.ts CHANGED
@@ -52,6 +52,8 @@ export interface LoraDriver {
52
52
  * Open an interactive transaction. Needed for mutations, which check
53
53
  * their writes (connect targets, cardinality, authorization) before
54
54
  * committing. Optional: the WASM binding has none, so it serves reads.
55
+ * A write transaction waits for the writer lock: the wait must honour
56
+ * `timeoutMs` and `signal` (`loraDriver` bounds it).
55
57
  */
56
58
  begin?(options: RunOptions): Promise<DriverTransaction>;
57
59
  /** Plan a statement without running it. Optional: the WASM binding has no `explain()`. */
package/dist/errors.d.ts CHANGED
@@ -15,6 +15,34 @@ export declare class ModelError extends Error {
15
15
  constructor(problems: ModelProblem[]);
16
16
  }
17
17
  export declare function formatProblem(p: ModelProblem): string;
18
- export type LoraGraphQLErrorCode = "BAD_USER_INPUT" | "INVALID_CURSOR" | "LIMIT_EXCEEDED" | "COST_EXCEEDED" | "UNAUTHENTICATED" | "FORBIDDEN" | "NOT_FOUND" | "CONSTRAINT_VIOLATION" | "DATABASE_ERROR" | "PERSISTED_QUERY_ONLY" | "WRONG_OPERATION_TYPE";
18
+ /**
19
+ * Every `extensions.code` a request-time error of this library carries, in
20
+ * a stable order. Use it to exhaustively map codes (HTTP status, client
21
+ * copy) or to test membership at runtime.
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"];
24
+ export type LoraGraphQLErrorCode = (typeof LORA_GRAPHQL_ERROR_CODES)[number];
25
+ /**
26
+ * Whether `err` is a request-time error raised by this library: a
27
+ * GraphQL error (or a plain `{ extensions: { code } }` from a serialized
28
+ * response) whose `extensions.code` is one of `LORA_GRAPHQL_ERROR_CODES`.
29
+ * Duck-typed, so it also recognizes errors built by another copy of
30
+ * `graphql`.
31
+ */
32
+ export declare function isLoraGraphQLError(err: unknown): err is {
33
+ message: string;
34
+ extensions: {
35
+ code: LoraGraphQLErrorCode;
36
+ };
37
+ };
19
38
  /** A request-time error with a stable `extensions.code`. */
20
39
  export declare function requestError(code: LoraGraphQLErrorCode, message: string, cause?: unknown, extensions?: Record<string, unknown>): GraphQLError;
40
+ /**
41
+ * One error per field path pattern: errors with the same message and code
42
+ * at paths that differ only in list indices (a field every row of a list
43
+ * refuses, such as an @authentication field read anonymously) become one
44
+ * error at the first path, with `extensions.count` and
45
+ * `extensions.pathPattern` (indices as `"*"`). A page of 100 x 100 rows
46
+ * otherwise answers with 10 000 copies of one error.
47
+ */
48
+ export declare function collapseErrors(errors: readonly GraphQLError[]): GraphQLError[];
@@ -0,0 +1,22 @@
1
+ /** Default `maxConcurrentStatements`. */
2
+ export declare const MAX_CONCURRENT_STATEMENTS = 2;
3
+ /** Default `operationTimeoutMs`, as a multiple of `timeoutMs`. */
4
+ export declare const OPERATION_TIMEOUT_FACTOR = 2;
5
+ export declare class OperationBudget {
6
+ #private;
7
+ /** Root fields of the operation in flight. */
8
+ active: number;
9
+ constructor(timeoutMs: number, maxConcurrent: number);
10
+ get signal(): AbortSignal;
11
+ /** Milliseconds left before the deadline (Infinity without one). */
12
+ remaining(): number;
13
+ /**
14
+ * Run `fn` once a statement slot is free, failing with TIMEOUT when the
15
+ * deadline passes first (the statement keeps its own abort signal).
16
+ */
17
+ run<T>(fn: (signal: AbortSignal) => Promise<T>): Promise<T>;
18
+ /** Stop the deadline timer: the operation is over. */
19
+ dispose(): void;
20
+ }
21
+ /** One signal aborted when either is; `b` alone when `a` is absent. */
22
+ export declare function anySignal(a: AbortSignal | undefined, b: AbortSignal): AbortSignal;
@@ -53,6 +53,8 @@ export interface WriteChange {
53
53
  export interface ReadSetLike {
54
54
  labels: readonly string[];
55
55
  relationships: readonly string[];
56
+ /** Reads nobody can name (a `@cypher` statement): every change affects it. */
57
+ opaque?: boolean | undefined;
56
58
  }
57
59
  /**
58
60
  * Whether a read with this read-set may observe the change. Label-level:
@@ -1,4 +1,7 @@
1
1
  import { FieldNode } from 'graphql';
2
+ import { Statement } from '../driver.js';
2
3
  import { CypherField } from '../model/types.js';
3
4
  import { MutationEnv } from './mutate.js';
4
- export declare function executeCypherMutation(env: MutationEnv, field: CypherField, args: Record<string, unknown>, fieldNodes: readonly FieldNode[]): Promise<unknown>;
5
+ export declare function executeCypherMutation(env: MutationEnv, field: CypherField, args: Record<string, unknown>, fieldNodes: readonly FieldNode[],
6
+ /** The field's `viewer` rules, checked in the transaction first. */
7
+ guard?: Statement): Promise<unknown>;
@@ -1,4 +1,5 @@
1
1
  import { FieldNode } from 'graphql';
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';
@@ -28,8 +29,21 @@ export interface MutationEnv {
28
29
  timeoutMs: number;
29
30
  signal: AbortSignal | undefined;
30
31
  degrees: ReadonlyMap<string, number>;
31
- /** Most nodes one mutation may create or delete, nested ones included. */
32
+ /**
33
+ * Most nodes one mutation may create, update or delete, nested ones
34
+ * included; relationships written, ten times as many.
35
+ */
32
36
  maxBatch: number;
37
+ /** Most items a @cypher list argument takes without `@size(max:)`. */
38
+ maxListArgument?: number | undefined;
39
+ /** Relationship levels one `where` may nest. */
40
+ maxFilterDepth?: number | undefined;
41
+ /** Items an `in` filter operand may hold. */
42
+ maxListFilter?: number | undefined;
43
+ /** Characters a string filter operand may hold. */
44
+ maxStringFilter?: number | undefined;
45
+ /** Statistics from `analyze()`, for filter costs. */
46
+ statistics?: Statistics | undefined;
33
47
  callbacks: Readonly<Record<string, PopulatedByCallback>>;
34
48
  /**
35
49
  * A caller-owned transaction: run inside it and leave the commit to the
@@ -40,6 +54,15 @@ export interface MutationEnv {
40
54
  onStatement?: ((statement: Statement) => void) | undefined;
41
55
  /** Runs a statement under timing, tracing and metrics, when configured. */
42
56
  observe?: ((statement: Statement, run: () => Promise<QueryResult>) => Promise<QueryResult>) | undefined;
57
+ /**
58
+ * Called inside the transaction once a delete knows every node it
59
+ * removes, before any is removed: subscribers' checks of the nodes as
60
+ * they were (a deleted node cannot be checked after the commit).
61
+ */
62
+ beforeDelete?: ((change: WriteChange, doomed: ReadonlyArray<{
63
+ node: NodeType;
64
+ keys: unknown[];
65
+ }>, run: (statement: Statement) => Promise<QueryResult>) => Promise<void>) | undefined;
43
66
  }
44
67
  /** Run `statement` in `tx`, reporting it before and observing it during. */
45
68
  export declare function runStatement(env: MutationEnv, tx: DriverTransaction, statement: Statement): Promise<QueryResult>;
@@ -50,6 +73,11 @@ export interface MutationInfo {
50
73
  relationshipsCreated: number;
51
74
  relationshipsDeleted: number;
52
75
  }
76
+ /**
77
+ * `@uniqueTogether` after a write whose write-set is unknown (a `@cypher`
78
+ * mutation): every node of each type in `nodes` is compared, in `tx`.
79
+ */
80
+ export declare function checkUniqueTogetherOf(env: MutationEnv, tx: DriverTransaction, nodes: readonly NodeType[]): Promise<void>;
53
81
  export interface MutationResult {
54
82
  payload: unknown;
55
83
  change: WriteChange;
package/dist/guards.d.ts CHANGED
@@ -41,8 +41,15 @@ export declare function parseOptions(guards?: DocumentGuards): ParseOptions;
41
41
  * The guards as an Envelop plugin, for GraphQL Yoga and other Envelop
42
42
  * servers: the token limit wraps `parse`, the rest are validation rules.
43
43
  * Typed structurally, so the package does not depend on Envelop.
44
+ *
45
+ * It also checks, once, that the server runs the same `graphql` copy as
46
+ * this library (see `graphqlRealmProblem`), and reports a mismatch
47
+ * through `onRealmMismatch` (default: `console.error`).
44
48
  */
45
- export declare function envelopPlugin(guards?: DocumentGuards): {
49
+ export declare function envelopPlugin(guards?: DocumentGuards, onRealmMismatch?: (message: string) => void): {
50
+ onSchemaChange({ schema }: {
51
+ schema: unknown;
52
+ }): void;
46
53
  onParse({ parseFn, setParseFn, }: {
47
54
  parseFn: (source: unknown, options?: ParseOptions) => unknown;
48
55
  setParseFn: (fn: (source: unknown, options?: ParseOptions) => unknown) => void;
@@ -51,4 +58,15 @@ export declare function envelopPlugin(guards?: DocumentGuards): {
51
58
  addValidationRule: (rule: ValidationRule) => void;
52
59
  }): void;
53
60
  };
61
+ type RealmProbe = "schema" | "validation";
62
+ /**
63
+ * When `value` (a schema, or the context a validation rule receives)
64
+ * comes from a different `graphql` module than the one this library
65
+ * imports, the message explaining it; otherwise undefined. Two copies
66
+ * break `instanceof GraphQLError`, so servers such as Yoga mask the
67
+ * library's errors (FORBIDDEN, BAD_USER_INPUT, ...) as "Unexpected
68
+ * error".
69
+ */
70
+ export declare function graphqlRealmProblem(value: unknown, what: RealmProbe): string | undefined;
54
71
  export declare function nodeEnv(): string | undefined;
72
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { LoraGraphQL, type AssertSchemaOptions, type CheckOptions, type CheckReport, type CostEvent, type DatabaseErrorEvent, type ExecuteArgs, type ExecutionTiming, type LoraGraphQLContext, type LoraGraphQLOptions, type SchemaAssertion, type StatementEvent, } from './lora-graphql.js';
1
+ export { LoraGraphQL, type AssertSchemaOptions, type CheckOptions, type CheckReport, type CostEvent, type DatabaseErrorEvent, type ExecuteArgs, type ExecutionTiming, type LoraGraphQLContext, type LoraGraphQLOptions, type LoraExecutionResult, type SchemaAssertion, type StatementEvent, } from './lora-graphql.js';
2
2
  export { loraDriver, type DriverTransaction, type LoraDatabaseLike, type LoraDriver, type QueryPlan, type QueryResult, type RunOptions, type Statement, } from './driver.js';
3
3
  export { directiveTypeDefs } from './model/directives.js';
4
4
  export { buildModel, type ModelOptions } from './model/build.js';
@@ -6,7 +6,7 @@ export type * from './model/types.js';
6
6
  export { inferRequirements, requirementDdl, type SchemaRequirement, } from './analyze/indexes.js';
7
7
  export { checkPlans, scanExpands, type PlanFinding, type PlanReport, } from './analyze/plans.js';
8
8
  export type { DegreeStats, Statistics } from './analyze/statistics.js';
9
- export type { AccessEntry, AccessVerdict } from './analyze/access.js';
9
+ export type { AccessEntry, AccessVerdict, OperationAccess, RootFieldAccess, } from './analyze/access.js';
10
10
  export type { CypherFinding } from './analyze/cypher-check.js';
11
11
  export { diffSchemas, type ApiChange, type SchemaDiff, } from './analyze/diff.js';
12
12
  export type { CompiledRead, ReadSet, SeekExpectation } from './compile/read.js';
@@ -15,7 +15,7 @@ export type { MutationInfo, PopulatedByCallback } from './execute/mutate.js';
15
15
  export type { MutationKind } from './schema/mutations.js';
16
16
  export type { ChangeEvent } from './schema/build.js';
17
17
  export { LoraTransaction } from './execute/transaction.js';
18
- export { ModelError, formatProblem, type LoraGraphQLErrorCode, type ModelProblem, } from './errors.js';
18
+ export { ModelError, formatProblem, isLoraGraphQLError, LORA_GRAPHQL_ERROR_CODES, type LoraGraphQLErrorCode, type ModelProblem, } from './errors.js';
19
19
  export { toGlobalId, fromGlobalId } from './schema/global-id.js';
20
20
  export type { Attributes, MetricsLike, ObservabilityOptions, SpanLike, StatementEndEvent, TracerLike, } from './observe.js';
21
21
  export { schemaHash, type ManifestOperation, type OperationManifest, } from './codegen.js';
package/dist/index.js CHANGED
@@ -1,24 +1,26 @@
1
- import { D as e, L as r, a as o, M as l, b as i, c as d, d as n, e as t, f, g as m, i as p, l as c, p as h, r as D, s as b, h as u, t as v, v as L } from "./driver-DAtBEMWj.js";
2
- import { d as x } from "./diff-CcRU-3_p.js";
1
+ import { D as r, L as e, a as o, b as l, M as i, c as d, d as n, e as t, f as p, g as f, h as m, i as L, j as c, l as R, p as h, r as D, s as E, k as G, t as b, v as u } from "./driver-C5dSoSD8.js";
2
+ import { d as A } from "./diff-B9iDdELF.js";
3
3
  export {
4
- e as DEFAULT_GUARDS,
5
- r as LoraGraphQL,
6
- o as LoraTransaction,
7
- l as ModelError,
8
- i as buildModel,
9
- d as checkPlans,
10
- x as diffSchemas,
11
- n as directiveTypeDefs,
12
- t as envelopPlugin,
4
+ r as DEFAULT_GUARDS,
5
+ e as LORA_GRAPHQL_ERROR_CODES,
6
+ o as LoraGraphQL,
7
+ l as LoraTransaction,
8
+ i as ModelError,
9
+ d as buildModel,
10
+ n as checkPlans,
11
+ A as diffSchemas,
12
+ t as directiveTypeDefs,
13
+ p as envelopPlugin,
13
14
  f as formatProblem,
14
15
  m as fromGlobalId,
15
- p as inferRequirements,
16
- c as loraDriver,
16
+ L as inferRequirements,
17
+ c as isLoraGraphQLError,
18
+ R as loraDriver,
17
19
  h as parseOptions,
18
20
  D as requirementDdl,
19
- b as scanExpands,
20
- u as schemaHash,
21
- v as toGlobalId,
22
- L as validationRules
21
+ E as scanExpands,
22
+ G as schemaHash,
23
+ b as toGlobalId,
24
+ u as validationRules
23
25
  };
24
26
  //# sourceMappingURL=index.js.map
@@ -1,8 +1,8 @@
1
- import { DocumentNode, ExecutionResult, GraphQLSchema, GraphQLFieldResolver, GraphQLScalarType, ValidationRule } from 'graphql';
1
+ import { ExecutionArgs, DocumentNode, ExecutionResult, GraphQLSchema, GraphQLFieldResolver, GraphQLScalarType, ValidationRule } from 'graphql';
2
2
  import { CypherFinding } from './analyze/cypher-check.js';
3
3
  import { SchemaRequirement } from './analyze/indexes.js';
4
4
  import { PlanReport } from './analyze/plans.js';
5
- import { AccessEntry } from './analyze/access.js';
5
+ import { AccessEntry, OperationAccess } from './analyze/access.js';
6
6
  import { Statistics } from './analyze/statistics.js';
7
7
  import { CompiledRead, ReadSet } from './compile/read.js';
8
8
  import { LoraDriver, Statement } from './driver.js';
@@ -20,10 +20,34 @@ export interface LoraGraphQLOptions extends ModelOptions, ObservabilityOptions {
20
20
  driver: LoraDriver;
21
21
  /** Timeout for every statement, in milliseconds. Default 10 000; 0 disables. */
22
22
  timeoutMs?: number;
23
+ /**
24
+ * Time budget for all root fields of one query, in milliseconds: past
25
+ * it, the operation's statements are aborted and its unfinished fields
26
+ * fail with TIMEOUT. Default twice `timeoutMs` (20 000); 0 disables.
27
+ * Subscriptions are not bounded by it; mutations keep `timeoutMs` per
28
+ * statement.
29
+ */
30
+ operationTimeoutMs?: number;
31
+ /**
32
+ * Statements one operation runs at once: aliased root fields queue for
33
+ * a slot instead of taking every libuv worker, so one request cannot
34
+ * starve the others. Default 2.
35
+ */
36
+ maxConcurrentStatements?: number;
37
+ /**
38
+ * Approximate bytes the compile cache may hold, and again the parsed
39
+ * document cache: entries are evicted oldest first past it, besides the
40
+ * entry caps. Requests with more than 16 KiB of variables a field uses
41
+ * (long `in:` lists, embedding vectors) are compiled but not cached.
42
+ * Default 64 MiB.
43
+ */
44
+ compileCacheBytes?: number;
23
45
  /**
24
46
  * Reject a root field whose estimated rows touched exceed this, before
25
47
  * it runs. The estimate multiplies page sizes through nested lists,
26
- * capped by relationship degrees from `analyze()` or `@cardinality`.
48
+ * capped by relationship degrees from `analyze()` or `@cardinality`,
49
+ * and charges each filter the rows it examines: a label scan when no
50
+ * index answers it, the related nodes of every relationship it follows.
27
51
  * Default 50 000; `Infinity` disables.
28
52
  */
29
53
  maxCost?: number;
@@ -41,16 +65,61 @@ export interface LoraGraphQLOptions extends ModelOptions, ObservabilityOptions {
41
65
  */
42
66
  jwt?: (context: unknown) => Record<string, unknown> | undefined;
43
67
  /**
44
- * Most nodes one mutation may create or delete, nested ones included.
45
- * Default 1000: larger imports belong in a Cypher load, not a GraphQL
68
+ * Most nodes one mutation may create, update or delete, nested ones
69
+ * included (and ten times as many relationships written). Default 1000: larger imports belong in a Cypher load, not a GraphQL
46
70
  * request. Also the default `limit` of bulk updates and deletes.
47
71
  */
48
72
  maxBatch?: number;
73
+ /**
74
+ * Most items a list argument of a `@cypher` field takes, unless its
75
+ * `@size(max:)` says otherwise. More is BAD_USER_INPUT before the
76
+ * statement runs. Default 1000.
77
+ */
78
+ maxListArgument?: number;
79
+ /**
80
+ * Relationship levels one `where` may nest: quantifiers (`some`,
81
+ * `none`, `all`, `single`, `count`, `aggregate`), connection filters,
82
+ * `<field>Exists` and filters through a single relationship each count
83
+ * one. Deeper is BAD_USER_INPUT before anything runs: each level
84
+ * multiplies the work by the relationship's degree. Default 2.
85
+ */
86
+ maxFilterDepth?: number;
87
+ /**
88
+ * Items an `in` filter operand may hold. More is BAD_USER_INPUT before
89
+ * anything runs. Default 1000.
90
+ */
91
+ maxListFilter?: number;
92
+ /**
93
+ * Characters a string filter operand (`eq`, `contains`, an `in` item, …)
94
+ * may hold. More is BAD_USER_INPUT before anything runs. Default 10 000.
95
+ */
96
+ maxStringFilter?: number;
49
97
  /**
50
98
  * Changes a `changes()` consumer or subscriber may fall behind before it
51
99
  * is ended with an error. Default 1000.
52
100
  */
53
101
  maxQueuedChanges?: number;
102
+ /**
103
+ * Live subscriptions one scope (see `subscriptionScope`) may hold; one
104
+ * more is LIMIT_EXCEEDED. Default 100.
105
+ */
106
+ maxSubscriptions?: number;
107
+ /**
108
+ * What `maxSubscriptions` counts per: by default the context object, so
109
+ * a server that reuses one context per connection (graphql-ws) limits
110
+ * each connection. Return the connection (or user) otherwise.
111
+ */
112
+ subscriptionScope?: (context: unknown) => object | undefined;
113
+ /**
114
+ * How deep relationship filters may nest in a subscription's `where`,
115
+ * which runs on every change. Deeper is LIMIT_EXCEEDED. Default 1.
116
+ */
117
+ maxSubscriptionFilterDepth?: number;
118
+ /**
119
+ * Per statement a subscription runs to check a change (visibility,
120
+ * `where`, related nodes). Default 2000, or `timeoutMs` when lower.
121
+ */
122
+ subscriptionTimeoutMs?: number;
54
123
  /** Named callbacks for `@populatedBy(callback:)`. */
55
124
  callbacks?: Record<string, PopulatedByCallback>;
56
125
  /**
@@ -99,9 +168,11 @@ export interface LoraGraphQLOptions extends ModelOptions, ObservabilityOptions {
99
168
  * later failure leaves the earlier ones committed. `"operation"`: every
100
169
  * root field in one transaction, committed only when the operation
101
170
  * reports no error, rolled back (with `data: null`) otherwise. A
102
- * `transaction` in the context takes precedence. Servers that call
103
- * graphql-js on `getSchema()` directly get per-operation atomicity by
104
- * putting a `lora.begin()` transaction in the context.
171
+ * `transaction` in the context takes precedence. Envelop / Yoga servers
172
+ * on `getSchema()` get the same with `lora.envelopPlugin()`; other
173
+ * servers calling graphql-js directly put a `lora.begin()` transaction
174
+ * in the context (without one, a one-time warning says each root field
175
+ * committed on its own).
105
176
  */
106
177
  mutationTransaction?: "field" | "operation";
107
178
  /**
@@ -244,15 +315,41 @@ export interface ExecuteArgs {
244
315
  variables?: Record<string, unknown>;
245
316
  operationName?: string;
246
317
  context?: unknown;
318
+ /**
319
+ * Attach the operation's read-set to the result as a non-enumerable
320
+ * `readSet` property (never serialized to the client), for a response
321
+ * cache to invalidate with `lora.affects(result.readSet, change)`.
322
+ */
323
+ readSet?: boolean;
247
324
  }
325
+ /** `execute()`'s result; `readSet` is there when `ExecuteArgs.readSet` asked. */
326
+ export type LoraExecutionResult = ExecutionResult & {
327
+ readonly readSet?: ReadSet;
328
+ };
248
329
  export declare class LoraGraphQL {
249
330
  #private;
250
331
  readonly model: GraphModel;
251
332
  constructor(options: LoraGraphQLOptions);
252
333
  /** The configured document guards as validation rules. */
253
334
  validationRules(): ValidationRule[];
254
- /** The configured document guards as an Envelop / Yoga plugin. */
255
- envelopPlugin(): ReturnType<typeof envelopPlugin>;
335
+ /**
336
+ * The configured document guards as an Envelop / Yoga plugin. With
337
+ * `mutationTransaction: "operation"` it also runs a mutation with
338
+ * several root fields in one transaction, and it collapses identical
339
+ * errors across list indices, as `execute()` does.
340
+ */
341
+ envelopPlugin(): ReturnType<typeof envelopPlugin> & {
342
+ onExecute(payload: {
343
+ args: ExecutionArgs;
344
+ executeFn: (args: ExecutionArgs) => unknown;
345
+ setExecuteFn: (fn: (args: ExecutionArgs) => unknown) => void;
346
+ }): {
347
+ onExecuteDone(payload: {
348
+ result: unknown;
349
+ setResult: (result: ExecutionResult) => void;
350
+ }): void;
351
+ };
352
+ };
256
353
  /** The executable schema, for any graphql-js server. */
257
354
  getSchema(): GraphQLSchema;
258
355
  /** The client-facing SDL: generated types only, no model directives. */
@@ -266,8 +363,10 @@ export declare class LoraGraphQL {
266
363
  */
267
364
  assertSchema(options?: AssertSchemaOptions): Promise<SchemaAssertion>;
268
365
  /**
269
- * Sample node counts and relationship degrees (S6). Cost estimates then
270
- * use each relationship's p99 degree instead of its page limit.
366
+ * Count nodes and measure relationship degrees (S6). Cost estimates then
367
+ * use each relationship's maximum degree, capped by its page limit,
368
+ * instead of the page limit alone; filters use node counts and mean
369
+ * degrees (see `src/compile/cost.ts`).
271
370
  */
272
371
  analyze(options?: {
273
372
  sample?: number;
@@ -286,6 +385,14 @@ export declare class LoraGraphQL {
286
385
  * model; stable, so it can be snapshotted and reviewed as a diff.
287
386
  */
288
387
  accessMatrix(): AccessEntry[];
388
+ /**
389
+ * Who may run an operation: per root field, the verdict for each kind
390
+ * of caller, from the same rules as `accessMatrix()`, plus the most
391
+ * restrictive verdict per caller. Takes a document (source or parsed)
392
+ * or the id of a persisted operation; `operationName` picks one of
393
+ * several operations.
394
+ */
395
+ operationAccess(document: string | DocumentNode, operationName?: string): OperationAccess;
289
396
  check(options?: CheckOptions): Promise<CheckReport>;
290
397
  /**
291
398
  * Compile a query without running it: one entry per root field, with its
@@ -332,7 +439,7 @@ export declare class LoraGraphQL {
332
439
  * subscription runs with {@link LoraGraphQL.subscribe}; given one, this
333
440
  * returns a `WRONG_OPERATION_TYPE` error.
334
441
  */
335
- execute(args: ExecuteArgs): Promise<ExecutionResult>;
442
+ execute(args: ExecuteArgs): Promise<LoraExecutionResult>;
336
443
  /**
337
444
  * Run a subscription: an async iterable of results, one per event, or a
338
445
  * single result with the errors when it cannot start (an unknown id, a
@@ -41,3 +41,13 @@ export declare function desugarRule(ctx: DesugarContext, owner: NodeType | undef
41
41
  export declare function desugarNode(ctx: DesugarContext, node: NodeType, where: unknown): unknown;
42
42
  /** Model problems for a `@viewer` mapping, once the node types are built. */
43
43
  export declare function checkViewer(viewer: ViewerMapping, nodes: ReadonlyMap<string, NodeType>, jwtType: string, problems: ModelProblem[]): void;
44
+ /**
45
+ * The parts a rule tests at its top level, through AND, OR and NOT:
46
+ * `node`, `jwt`, `viewer`, `source`, `target`, `edge`.
47
+ */
48
+ export declare function ruleParts(where: unknown, out?: Set<string>): Set<string>;
49
+ /**
50
+ * A relationship property's rule that tests the relationship's ends or
51
+ * edge (`source`, `target`, `edge`): decided per relationship.
52
+ */
53
+ export declare function testsRelationshipEnds(where: unknown): boolean;
@@ -1,3 +1,3 @@
1
- export declare const directiveTypeDefs = "\n \"A node label set. Defaults to the type name.\"\n directive @node(labels: [String!], plural: String) on OBJECT\n\n \"Natural key: required, unique and immutable; the tie-breaker of every sort and the anchor of cursors, global ids, updates and deletes. With generate: true, creates fill it with a UUID when the input leaves it out.\"\n directive @key(\n generate: Boolean = false\n \"VIEWER: a created key must start with the caller's @viewer claim and the separator (lou:tomorrowland).\"\n scope: KeyScope\n separator: String = \":\"\n ) on FIELD_DEFINITION\n\n enum KeyScope {\n VIEWER\n }\n\n \"On an interface field: every implementation declares this relationship (with @relationship, possibly of different types or directions), so clients can select it on the interface.\"\n directive @declareRelationship on FIELD_DEFINITION\n\n \"Offer this field as a grouping key of <plural>Grouped (needs @query(aggregate: true)).\"\n directive @groupBy on FIELD_DEFINITION\n\n \"A field computed in JavaScript by the resolver passed in the resolvers option. requires is a selection on this type (for example name capacity) fetched in the same statement, so the resolver reads it from its source.\"\n directive @customResolver(requires: String) on FIELD_DEFINITION\n\n \"On a custom scalar: how its values are stored. Filters, sorts and indexes follow the storage type; the scalar is passed through unless the scalars option supplies an implementation.\"\n directive @storedAs(type: StorageType!) on SCALAR\n\n enum StorageType {\n STRING\n INT\n FLOAT\n BOOLEAN\n DATETIME\n DATE\n }\n\n \"A uniqueness constraint.\"\n directive @unique on FIELD_DEFINITION\n\n \"An explicit index. Usually unnecessary: inferred from @filterable and @sortable.\"\n directive @index(kind: IndexKind!) on FIELD_DEFINITION\n\n \"An edge. The target type is the field's type.\"\n directive @relationship(\n type: String!\n direction: RelationshipDirection!\n properties: String\n \"UNDIRECTED reads follow the relationship both ways; writes use direction.\"\n queryDirection: QueryDirection = DIRECTED\n \"What deleting this node does to nodes reached through the field.\"\n onDelete: OnDelete = DETACH\n \"Which nested writes mutation inputs offer for the field.\"\n nestedOperations: [NestedOperation!] = [\n CREATE\n CONNECT\n DISCONNECT\n UPDATE\n DELETE\n ]\n \"false: no aggregate on the field's connection and no aggregate filter.\"\n aggregate: Boolean = true\n ) on FIELD_DEFINITION\n\n enum NestedOperation {\n CREATE\n CONNECT\n DISCONNECT\n UPDATE\n DELETE\n }\n\n enum QueryDirection {\n DIRECTED\n UNDIRECTED\n }\n\n \"DETACH removes the relationships; CASCADE deletes the related nodes too; RESTRICT refuses while any exist.\"\n enum OnDelete {\n DETACH\n CASCADE\n RESTRICT\n }\n\n \"Which mutations may set the field. @readonly is onCreate: false, onUpdate: false.\"\n directive @settable(\n onCreate: Boolean = true\n onUpdate: Boolean = true\n ) on FIELD_DEFINITION\n\n \"Whether the field can be read (onRead: false makes it write-only) or aggregated.\"\n directive @selectable(\n onRead: Boolean = true\n onAggregate: Boolean = true\n ) on FIELD_DEFINITION\n\n \"Set by a callback passed to LoraGraphQL({ callbacks }) on these operations; never client-settable.\"\n directive @populatedBy(\n callback: String!\n operations: [TimestampOperation!]! = [CREATE, UPDATE]\n ) on FIELD_DEFINITION\n\n \"The shape of the request's claims. Rules may only test declared claims.\"\n directive @jwt on OBJECT\n\n \"Where a declared claim lives in the token, e.g. app_metadata.roles.\"\n directive @jwtClaim(path: String!) on FIELD_DEFINITION\n\n \"The claim that identifies the caller: the node of `type` whose `field` (a @key or @unique field) equals it. Enables isViewer and viewer in rules.\"\n directive @viewer(type: String!, field: String!) on FIELD_DEFINITION\n\n \"Properties carried by a relationship type.\"\n directive @relationshipProperties on OBJECT\n\n \"API name differs from the stored property.\"\n directive @alias(property: String!) on FIELD_DEFINITION\n\n \"Stored, never exposed.\"\n directive @private on FIELD_DEFINITION\n\n \"Declared upper bound on a relationship's fan-out, used by cost estimates.\"\n directive @cardinality(max: Int!) on FIELD_DEFINITION\n\n \"Generated read operations for a node type. Reads are on by default.\"\n directive @query(\n read: Boolean = true\n aggregate: Boolean = false\n ) on OBJECT | INTERFACE | UNION\n\n \"The plural of an interface or union, for its root field.\"\n directive @plural(value: String!) on INTERFACE | UNION\n\n \"Filter operators for a field. Without arguments: EQ and IN. On a relationship field: enables relationship filters.\"\n directive @filterable(byValue: [FilterOperator!]) on FIELD_DEFINITION\n\n \"Sort and keyset-paginate on this field.\"\n directive @sortable on FIELD_DEFINITION\n\n \"Page size bounds for lists of this type.\"\n directive @limit(\n default: Int\n max: Int\n ) on OBJECT | INTERFACE | UNION | FIELD_DEFINITION\n\n \"Expose an opaque global id derived from this @key field.\"\n directive @relayId on FIELD_DEFINITION\n\n \"Full-text search over String fields: a FULLTEXT index and a search root field per entry.\"\n directive @fulltext(indexes: [FulltextIndex!]!) on OBJECT\n\n \"A vector embedding ([Float!]): a VECTOR index and a similarity root field.\"\n directive @vector(\n dimensions: Int!\n similarity: VectorSimilarity = COSINE\n queryName: String\n ) on FIELD_DEFINITION\n\n input FulltextIndex {\n \"Index name. Default: <label>_search for the first, required after.\"\n name: String\n fields: [String!]!\n analyzer: FulltextAnalyzer = STANDARD\n \"Root field name. Default: search<Plural>, or search<Plural>By<Name>.\"\n queryName: String\n }\n\n enum FulltextAnalyzer {\n STANDARD\n SIMPLE\n }\n\n enum VectorSimilarity {\n COSINE\n EUCLIDEAN\n }\n\n \"Generated subscriptions to changes made through the library. None without this directive.\"\n directive @subscription(\n operations: [MutationOperation!]! = [CREATE, UPDATE, DELETE]\n \"Also send CONNECT and DISCONNECT events for the type's relationships.\"\n relationships: Boolean = false\n \"Send the stored values before an update or delete (one extra read per write).\"\n previousState: Boolean = false\n ) on OBJECT\n\n \"Generated mutations for a node type. None without this directive.\"\n directive @mutation(\n operations: [MutationOperation!]! = [CREATE, UPDATE, DELETE]\n ) on OBJECT\n\n \"Value stored on create when the input leaves the field out.\"\n directive @default(value: DefaultValue!) on FIELD_DEFINITION\n\n \"Set to the current time by the listed operations; never client-settable.\"\n directive @timestamp(\n operations: [TimestampOperation!]! = [CREATE, UPDATE]\n ) on FIELD_DEFINITION\n\n \"Readable but never client-settable.\"\n directive @readonly on FIELD_DEFINITION\n\n \"A field backed by a Cypher statement. `this` is the parent node; arguments are $parameters, and $jwt holds the request's claims.\"\n directive @cypher(statement: String!, columnName: String) on FIELD_DEFINITION\n\n \"Require an authenticated request (a jwt in the context) for these operations.\"\n directive @authentication(\n operations: [AuthOperation!]! = [\n READ\n CREATE\n UPDATE\n DELETE\n CREATE_RELATIONSHIP\n DELETE_RELATIONSHIP\n SUBSCRIBE\n ]\n \"Claims the request must also satisfy, such as a role in roles.\"\n jwt: AuthorizationWhere\n ) on OBJECT | FIELD_DEFINITION\n\n \"Row-level rules over the node and the request's claims, compiled into the statements.\"\n directive @authorization(\n filter: [AuthorizationFilterRule!]\n validate: [AuthorizationValidateRule!]\n \"On a type: false keeps the schema's bypass (see @authorizationDefaults) from skipping this type's rules.\"\n bypass: Boolean\n \"On a @mutation type: operations deliberately open to every caller, so check() does not report them as unguarded.\"\n public: [AuthOperation!]\n \"On a scalar field: a row failing unless reads the field as value (null when left out) instead of failing the request.\"\n mask: [AuthorizationMask!]\n ) on OBJECT | FIELD_DEFINITION\n\n \"Named rules over claims, usable in any rule as { rule: name }, on `extend schema`.\"\n directive @authorizationRules(\n rules: [AuthorizationRuleDefinition!]!\n ) on SCHEMA\n\n input AuthorizationRuleDefinition {\n name: String!\n where: AuthorizationWhere!\n }\n\n \"A named rule of this type, usable in its rules as { rule: name } and through a relationship to it as { rel: { rule: name } }.\"\n directive @authorizationRule(\n name: String!\n where: AuthorizationWhere!\n ) repeatable on OBJECT\n\n \"Schema-wide authorization settings, on `extend schema`.\"\n directive @authorizationDefaults(\n \"A claims-only test (jwt, AND, OR, NOT): a request passing it skips every filter and validate rule (not @authentication).\"\n bypass: AuthorizationWhere\n \"The write rule of every @mutation type that declares no CREATE, UPDATE or DELETE rule of its own.\"\n mutations: AuthorizationWhere\n ) on SCHEMA\n\n input AuthorizationMask {\n unless: AuthorizationWhere!\n value: DefaultValue\n }\n\n input AuthorizationFilterRule {\n operations: [AuthOperation!]! = [READ, UPDATE, DELETE]\n \"Default true: without a token the rule denies.\"\n requireAuthentication: Boolean\n where: AuthorizationWhere!\n }\n\n input AuthorizationValidateRule {\n operations: [AuthOperation!]! = [READ, CREATE, UPDATE, DELETE]\n when: [AuthorizationWhen!]! = [BEFORE, AFTER]\n \"Default true: without a token the rule denies.\"\n requireAuthentication: Boolean\n where: AuthorizationWhere!\n }\n\n enum MutationOperation {\n CREATE\n UPDATE\n DELETE\n }\n\n enum TimestampOperation {\n CREATE\n UPDATE\n }\n\n enum AuthOperation {\n READ\n CREATE\n UPDATE\n DELETE\n CREATE_RELATIONSHIP\n DELETE_RELATIONSHIP\n SUBSCRIBE\n \"On a relationship field: creating one of its relationships (connect, nested create).\"\n CONNECT\n \"On a relationship field: removing one of its relationships (disconnect).\"\n DISCONNECT\n \"On a relationship field: setting properties on an existing relationship (edge update, re-connect).\"\n UPDATE_EDGE\n \"On a relationship field: reading, filtering, sorting or aggregating its properties.\"\n READ_EDGE\n }\n\n enum AuthorizationWhen {\n BEFORE\n AFTER\n }\n\n \"Any literal.\"\n scalar DefaultValue\n\n \"{ node: <Type>Where-shaped filter, jwt: claim filter, AND, OR, NOT }. String values starting with $jwt. are replaced by claims.\"\n scalar AuthorizationWhere\n\n enum IndexKind {\n RANGE\n TEXT\n POINT\n }\n\n enum RelationshipDirection {\n IN\n OUT\n }\n\n enum FilterOperator {\n EQ\n IN\n LT\n LTE\n GT\n GTE\n CONTAINS\n STARTS_WITH\n ENDS_WITH\n WITHIN_BBOX\n DISTANCE\n INCLUDES\n IS_NULL\n CASE_INSENSITIVE\n }\n\n scalar BigInt\n scalar Date\n scalar Time\n scalar LocalTime\n scalar DateTime\n scalar LocalDateTime\n scalar Duration\n\n type Point {\n longitude: Float!\n latitude: Float!\n height: Float\n srid: Int!\n crs: String!\n }\n\n type CartesianPoint {\n x: Float!\n y: Float!\n z: Float\n srid: Int!\n crs: String!\n }\n";
1
+ export declare const directiveTypeDefs = "\n \"A node label set. Defaults to the type name.\"\n directive @node(labels: [String!], plural: String) on OBJECT\n\n \"Natural key: required, unique and immutable; the tie-breaker of every sort and the anchor of cursors, global ids, updates and deletes. With generate: true, creates fill it with a UUID when the input leaves it out.\"\n directive @key(\n generate: Boolean = false\n \"VIEWER: a created key must start with the caller's @viewer claim and the separator (lou:tomorrowland).\"\n scope: KeyScope\n separator: String = \":\"\n ) on FIELD_DEFINITION\n\n enum KeyScope {\n VIEWER\n }\n\n \"On an interface field: every implementation declares this relationship (with @relationship, possibly of different types or directions), so clients can select it on the interface.\"\n directive @declareRelationship on FIELD_DEFINITION\n\n \"Offer this field as a grouping key of <plural>Grouped (needs @query(aggregate: true)).\"\n directive @groupBy on FIELD_DEFINITION\n\n \"A field computed in JavaScript by the resolver passed in the resolvers option. requires is a selection on this type (for example name capacity) fetched in the same statement, so the resolver reads it from its source.\"\n directive @customResolver(requires: String) on FIELD_DEFINITION\n\n \"On a custom scalar: how its values are stored. Filters, sorts and indexes follow the storage type; the scalar is passed through unless the scalars option supplies an implementation.\"\n directive @storedAs(type: StorageType!) on SCALAR\n\n enum StorageType {\n STRING\n INT\n FLOAT\n BOOLEAN\n DATETIME\n DATE\n }\n\n \"A uniqueness constraint.\"\n directive @unique on FIELD_DEFINITION\n\n \"No two nodes of this type (among those matching where) share the values of fields: scalar fields, single relationships (by the target's @key) and at most one list relationship (by the set of target keys). Enforced by every generated mutation, for every caller.\"\n directive @uniqueTogether(\n fields: [String!]!\n where: NodeWhere\n ) repeatable on OBJECT\n\n \"An explicit index. Usually unnecessary: inferred from @filterable and @sortable.\"\n directive @index(kind: IndexKind!) on FIELD_DEFINITION\n\n \"An edge. The target type is the field's type.\"\n directive @relationship(\n type: String!\n direction: RelationshipDirection!\n properties: String\n \"UNDIRECTED reads follow the relationship both ways; writes use direction.\"\n queryDirection: QueryDirection = DIRECTED\n \"What deleting this node does to nodes reached through the field.\"\n onDelete: OnDelete = DETACH\n \"Which nested writes mutation inputs offer for the field.\"\n nestedOperations: [NestedOperation!] = [\n CREATE\n CONNECT\n DISCONNECT\n UPDATE\n DELETE\n ]\n \"false: no aggregate on the field's connection and no aggregate filter.\"\n aggregate: Boolean = true\n ) on FIELD_DEFINITION\n\n \"UPDATE changes a connected node and its relationship properties in place; UPDATE_EDGE only the properties.\"\n enum NestedOperation {\n CREATE\n CONNECT\n DISCONNECT\n UPDATE\n UPDATE_EDGE\n DELETE\n }\n\n enum QueryDirection {\n DIRECTED\n UNDIRECTED\n }\n\n \"DETACH removes the relationships; CASCADE deletes the related nodes too; RESTRICT refuses while any exist.\"\n enum OnDelete {\n DETACH\n CASCADE\n RESTRICT\n }\n\n \"Which mutations may set the field. @readonly is onCreate: false, onUpdate: false.\"\n directive @settable(\n onCreate: Boolean = true\n onUpdate: Boolean = true\n ) on FIELD_DEFINITION\n\n \"Whether the field can be read (onRead: false makes it write-only) or aggregated.\"\n directive @selectable(\n onRead: Boolean = true\n onAggregate: Boolean = true\n ) on FIELD_DEFINITION\n\n \"Set by a callback passed to LoraGraphQL({ callbacks }) on these operations. Client-settable only with an explicit @settable(onCreate: true) or (onUpdate: true) and a field-level @authorization(validate:) rule for that operation.\"\n directive @populatedBy(\n callback: String!\n operations: [TimestampOperation!]! = [CREATE, UPDATE]\n ) on FIELD_DEFINITION\n\n \"The shape of the request's claims. Rules may only test declared claims.\"\n directive @jwt on OBJECT\n\n \"Where a declared claim lives in the token, e.g. app_metadata.roles.\"\n directive @jwtClaim(path: String!) on FIELD_DEFINITION\n\n \"The claim that identifies the caller: the node of `type` whose `field` (a @key or @unique field) equals it. Enables isViewer and viewer in rules.\"\n directive @viewer(type: String!, field: String!) on FIELD_DEFINITION\n\n \"Properties carried by a relationship type.\"\n directive @relationshipProperties on OBJECT\n\n \"API name differs from the stored property.\"\n directive @alias(property: String!) on FIELD_DEFINITION\n\n \"Stored, never exposed.\"\n directive @private on FIELD_DEFINITION\n\n \"Declared upper bound on a relationship's fan-out, used by cost estimates.\"\n directive @cardinality(max: Int!) on FIELD_DEFINITION\n\n \"The most items a list argument of a @cypher field takes; more is BAD_USER_INPUT before the statement runs. Without it, maxListArgument applies.\"\n directive @size(max: Int!) on ARGUMENT_DEFINITION\n\n \"Bounds of an Int or Float argument of a @cypher field (each item, for a list); a value outside them is BAD_USER_INPUT before the statement runs.\"\n directive @range(min: Float, max: Float) on ARGUMENT_DEFINITION\n\n \"Generated read operations for a node type. Reads are on by default.\"\n directive @query(\n read: Boolean = true\n aggregate: Boolean = false\n ) on OBJECT | INTERFACE | UNION\n\n \"The plural of an interface or union, for its root field.\"\n directive @plural(value: String!) on INTERFACE | UNION\n\n \"Filter operators for a field. Without arguments: EQ and IN. On a relationship field: enables relationship filters.\"\n directive @filterable(byValue: [FilterOperator!]) on FIELD_DEFINITION\n\n \"Sort and keyset-paginate on this field.\"\n directive @sortable on FIELD_DEFINITION\n\n \"Page size bounds for lists of this type.\"\n directive @limit(\n default: Int\n max: Int\n ) on OBJECT | INTERFACE | UNION | FIELD_DEFINITION\n\n \"Expose an opaque global id derived from this @key field.\"\n directive @relayId on FIELD_DEFINITION\n\n \"Full-text search over String fields: a FULLTEXT index and a search root field per entry.\"\n directive @fulltext(indexes: [FulltextIndex!]!) on OBJECT\n\n \"A vector embedding ([Float!]): a VECTOR index and a similarity root field.\"\n directive @vector(\n dimensions: Int!\n similarity: VectorSimilarity = COSINE\n queryName: String\n ) on FIELD_DEFINITION\n\n input FulltextIndex {\n \"Index name. Default: <label>_search for the first, required after.\"\n name: String\n fields: [String!]!\n analyzer: FulltextAnalyzer = STANDARD\n \"Root field name. Default: search<Plural>, or search<Plural>By<Name>.\"\n queryName: String\n }\n\n enum FulltextAnalyzer {\n STANDARD\n SIMPLE\n }\n\n enum VectorSimilarity {\n COSINE\n EUCLIDEAN\n }\n\n \"Generated subscriptions to changes made through the library. None without this directive.\"\n directive @subscription(\n operations: [MutationOperation!]! = [CREATE, UPDATE, DELETE]\n \"Also send CONNECT and DISCONNECT events for the type's relationships.\"\n relationships: Boolean = false\n \"Send the stored values before an update or delete (one extra read per write).\"\n previousState: Boolean = false\n ) on OBJECT\n\n \"Generated mutations for a node type. None without this directive.\"\n directive @mutation(\n operations: [MutationOperation!]! = [CREATE, UPDATE, DELETE]\n ) on OBJECT\n\n \"Value stored on create when the input leaves the field out.\"\n directive @default(value: DefaultValue!) on FIELD_DEFINITION\n\n \"Set to the current time by the listed operations. Client-settable only with an explicit @settable(onCreate: true) or (onUpdate: true) and a field-level @authorization(validate:) rule for that operation.\"\n directive @timestamp(\n operations: [TimestampOperation!]! = [CREATE, UPDATE]\n ) on FIELD_DEFINITION\n\n \"Readable but never client-settable.\"\n directive @readonly on FIELD_DEFINITION\n\n \"A field backed by a Cypher statement. `this` is the parent node; arguments are $parameters, $jwt holds the request's claims, and $viewer the caller's @viewer node key.\"\n directive @cypher(statement: String!, columnName: String) on FIELD_DEFINITION\n\n \"Require an authenticated request (a jwt in the context) for these operations.\"\n directive @authentication(\n operations: [AuthOperation!]! = [\n READ\n CREATE\n UPDATE\n DELETE\n CREATE_RELATIONSHIP\n DELETE_RELATIONSHIP\n SUBSCRIBE\n ]\n \"Claims the request must also satisfy, such as a role in roles.\"\n jwt: AuthorizationWhere\n ) on OBJECT | FIELD_DEFINITION\n\n \"Row-level rules over the node and the request's claims, compiled into the statements.\"\n directive @authorization(\n filter: [AuthorizationFilterRule!]\n validate: [AuthorizationValidateRule!]\n \"On a type: false keeps the schema's bypass (see @authorizationDefaults) from skipping this type's rules; true lets it skip them, as without the argument, and tells check() that was meant.\"\n bypass: Boolean\n \"On a @mutation type: operations deliberately open to every caller, so check() does not report them as unguarded.\"\n public: [AuthOperation!]\n \"On a scalar field: a row failing unless reads the field as value (null when left out) instead of failing the request.\"\n mask: [AuthorizationMask!]\n ) on OBJECT | FIELD_DEFINITION\n\n \"Named rules over claims, usable in any rule as { rule: name }, on `extend schema`.\"\n directive @authorizationRules(\n rules: [AuthorizationRuleDefinition!]!\n ) on SCHEMA\n\n input AuthorizationRuleDefinition {\n name: String!\n where: AuthorizationWhere!\n }\n\n \"A named rule of this type, usable in its rules as { rule: name } and through a relationship to it as { rel: { rule: name } }.\"\n directive @authorizationRule(\n name: String!\n where: AuthorizationWhere!\n ) repeatable on OBJECT\n\n \"Schema-wide authorization settings, on `extend schema`.\"\n directive @authorizationDefaults(\n \"A claims-only test (jwt, AND, OR, NOT): a request passing it skips every filter and validate rule (not @authentication).\"\n bypass: AuthorizationWhere\n \"The write rule of every @mutation type that declares no CREATE, UPDATE or DELETE rule of its own.\"\n mutations: AuthorizationWhere\n ) on SCHEMA\n\n input AuthorizationMask {\n unless: AuthorizationWhere!\n value: DefaultValue\n }\n\n input AuthorizationFilterRule {\n operations: [AuthOperation!]! = [READ, UPDATE, DELETE]\n \"Default true: without a token the rule denies.\"\n requireAuthentication: Boolean\n where: AuthorizationWhere!\n }\n\n input AuthorizationValidateRule {\n operations: [AuthOperation!]! = [READ, CREATE, UPDATE, DELETE]\n when: [AuthorizationWhen!]! = [BEFORE, AFTER]\n \"Default true: without a token the rule denies.\"\n requireAuthentication: Boolean\n where: AuthorizationWhere!\n }\n\n enum MutationOperation {\n CREATE\n UPDATE\n DELETE\n }\n\n enum TimestampOperation {\n CREATE\n UPDATE\n }\n\n enum AuthOperation {\n READ\n CREATE\n UPDATE\n DELETE\n CREATE_RELATIONSHIP\n DELETE_RELATIONSHIP\n SUBSCRIBE\n \"On a relationship field: creating one of its relationships (connect, nested create).\"\n CONNECT\n \"On a relationship field: removing one of its relationships (disconnect).\"\n DISCONNECT\n \"On a relationship field: setting properties on an existing relationship (edge update, re-connect).\"\n UPDATE_EDGE\n \"On a relationship field: reading, filtering, sorting or aggregating its properties.\"\n READ_EDGE\n }\n\n enum AuthorizationWhen {\n BEFORE\n AFTER\n }\n\n \"Any literal.\"\n scalar DefaultValue\n\n \"{ node: <Type>Where-shaped filter, jwt: claim filter, AND, OR, NOT }. String values starting with $jwt. are replaced by claims.\"\n scalar AuthorizationWhere\n\n \"A <Type>Where-shaped filter over the type the directive is on.\"\n scalar NodeWhere\n\n enum IndexKind {\n RANGE\n TEXT\n POINT\n }\n\n enum RelationshipDirection {\n IN\n OUT\n }\n\n enum FilterOperator {\n EQ\n IN\n LT\n LTE\n GT\n GTE\n CONTAINS\n STARTS_WITH\n ENDS_WITH\n WITHIN_BBOX\n DISTANCE\n INCLUDES\n IS_NULL\n CASE_INSENSITIVE\n }\n\n scalar BigInt\n scalar Date\n scalar Time\n scalar LocalTime\n scalar DateTime\n scalar LocalDateTime\n scalar Duration\n\n type Point {\n longitude: Float!\n latitude: Float!\n height: Float\n srid: Int!\n crs: String!\n }\n\n type CartesianPoint {\n x: Float!\n y: Float!\n z: Float\n srid: Int!\n crs: String!\n }\n";
2
2
  /** Names defined by the prelude; never treated as user types. */
3
3
  export declare const PRELUDE_TYPES: Set<string>;
@@ -10,3 +10,12 @@ export declare function isUpdatable(model: GraphModel, node: NodeType, seen?: Se
10
10
  export declare function offersNested(model: GraphModel, rel: RelationshipField, update: boolean, seen?: Set<string>): boolean;
11
11
  /** Whether any property of a @relationshipProperties type is settable on `op`. */
12
12
  export declare function hasSettable(props: RelationshipPropertiesType, op: "CREATE" | "UPDATE"): boolean;
13
+ /**
14
+ * The first value in `value` (each item, for a list) outside `range`, or
15
+ * undefined. Null and absent values are not checked: nullability is the
16
+ * type's business.
17
+ */
18
+ export declare function outOfRange(value: unknown, range: {
19
+ min?: number | undefined;
20
+ max?: number | undefined;
21
+ }): number | undefined;
@@ -84,7 +84,11 @@ export interface ScalarField extends FieldBase {
84
84
  } | undefined;
85
85
  description: string | undefined;
86
86
  }
87
- export type NestedOperation = "CREATE" | "CONNECT" | "DISCONNECT" | "UPDATE" | "DELETE";
87
+ export type NestedOperation = "CREATE" | "CONNECT" | "DISCONNECT"
88
+ /** `update: [{ key, edge, node }]`: the connected node and the properties. */
89
+ | "UPDATE"
90
+ /** `update: [{ key, edge }]`: the relationship's properties only. */
91
+ | "UPDATE_EDGE" | "DELETE";
88
92
  export interface RelationshipField extends FieldBase {
89
93
  kind: "relationship";
90
94
  name: string;
@@ -140,6 +144,13 @@ export interface CypherArgument {
140
144
  type: TypeShape;
141
145
  defaultValue: unknown;
142
146
  description: string | undefined;
147
+ /** `@size(max:)`: the most items a list argument takes. */
148
+ maxItems?: number | undefined;
149
+ /** `@range(min:, max:)`: bounds of an Int or Float argument. */
150
+ range?: {
151
+ min?: number | undefined;
152
+ max?: number | undefined;
153
+ } | undefined;
143
154
  }
144
155
  export interface CypherField extends FieldBase {
145
156
  kind: "cypher";
@@ -180,8 +191,15 @@ export interface CustomField extends FieldBase {
180
191
  description: string | undefined;
181
192
  }
182
193
  export type Field = ScalarField | RelationshipField | CypherField | CustomField;
183
- /** A `${jwt.path}` / `${context.path}` placeholder inside a rule string. */
194
+ /**
195
+ * A placeholder inside a rule string: `${jwt.path}`, `${context.path}`,
196
+ * `${viewer.field}`, or a value of the node the rule is about:
197
+ * `${node.path}` in a type's rules, `${source.path}` / `${target.path}` /
198
+ * `${edge.property}` in a relationship field's rules.
199
+ */
184
200
  export declare const PLACEHOLDER: RegExp;
201
+ /** The placeholder sources that name a node (or edge) of the rule itself. */
202
+ export declare const RULE_REFERENCES: Set<string>;
185
203
  /** `{ node, jwt, AND, OR, NOT }`, as written in `@authorization`. */
186
204
  export type AuthorizationWhere = Record<string, unknown>;
187
205
  export interface AuthorizationFilterRule {
@@ -202,7 +220,10 @@ export interface AuthorizationValidateRule {
202
220
  export interface Authorization {
203
221
  filter: readonly AuthorizationFilterRule[];
204
222
  validate: readonly AuthorizationValidateRule[];
205
- /** `bypass: false`: the schema's bypass does not skip these rules. */
223
+ /**
224
+ * `bypass: false`: the schema's bypass does not skip these rules.
225
+ * `true`: it does, as when left out, and `check()` does not note it.
226
+ */
206
227
  bypass?: boolean;
207
228
  /** `public:` operations deliberately open to every caller. */
208
229
  public?: ReadonlySet<AuthOperation>;
@@ -248,8 +269,28 @@ export interface NodeType {
248
269
  search: readonly SearchIndex[];
249
270
  /** Interfaces the type implements. */
250
271
  interfaces: readonly string[];
272
+ /** `@uniqueTogether`: combinations no two nodes of the type may share. */
273
+ uniqueTogether: readonly UniqueTogether[];
251
274
  description: string | undefined;
252
275
  }
276
+ /**
277
+ * `@uniqueTogether(fields:, where:)`: no two nodes of the type (among those
278
+ * matching `where`) hold the same value of every field. A combination
279
+ * with a null scalar, a missing single relationship or an empty set is
280
+ * exempt, as a null is in a unique index.
281
+ */
282
+ export interface UniqueTogether {
283
+ /** The fields as declared, for messages. */
284
+ fields: readonly string[];
285
+ /** Scalar fields, compared by stored value. */
286
+ scalars: readonly ScalarField[];
287
+ /** Single relationships, compared by the target's @key. */
288
+ singles: readonly RelationshipField[];
289
+ /** At most one list relationship, compared as the set of target keys. */
290
+ set: RelationshipField | undefined;
291
+ /** A `<Type>Where`-shaped filter: only nodes matching it are compared. */
292
+ where: Record<string, unknown> | undefined;
293
+ }
253
294
  export type SearchIndex = {
254
295
  kind: "fulltext";
255
296
  name: string;
@@ -346,6 +387,8 @@ export interface GraphModel {
346
387
  } | undefined;
347
388
  /** Secret cursors are signed with, when configured. */
348
389
  cursorSecret: string | undefined;
390
+ /** The global page-size cap (`maxLimit`); also caps `nodes(ids:)`. */
391
+ maxLimit?: number | undefined;
349
392
  }
350
393
  export interface ModelWarning {
351
394
  type: string;
@@ -0,0 +1,3 @@
1
+ import { GraphModel, NodeType } from './types.js';
2
+ /** The `@uniqueTogether` types `statement` may write. */
3
+ export declare function uniqueTogetherTouched(model: GraphModel, statement: string): NodeType[];