@loradb/lora-graphql 0.24.0 → 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 (53) hide show
  1. package/README.md +185 -42
  2. package/dist/analyze/access.d.ts +11 -3
  3. package/dist/analyze/input-schemas.d.ts +11 -0
  4. package/dist/analyze/plans.d.ts +8 -0
  5. package/dist/analyze/stored.d.ts +29 -0
  6. package/dist/analyze/verdicts.d.ts +6 -0
  7. package/dist/cli.js +263 -244
  8. package/dist/cli.js.map +1 -1
  9. package/dist/compile/cache.d.ts +9 -1
  10. package/dist/compile/context.d.ts +3 -0
  11. package/dist/compile/read/common.d.ts +2 -0
  12. package/dist/compile/read/root.d.ts +7 -0
  13. package/dist/compile/selection.d.ts +5 -0
  14. package/dist/diff-CdZZG-N1.js +134 -0
  15. package/dist/diff-CdZZG-N1.js.map +1 -0
  16. package/dist/driver.d.ts +36 -1
  17. package/dist/errors.d.ts +21 -1
  18. package/dist/events/outbox.d.ts +14 -0
  19. package/dist/events/worker.d.ts +67 -0
  20. package/dist/execute/mutate/checks.d.ts +16 -0
  21. package/dist/execute/mutate/env.d.ts +50 -0
  22. package/dist/execute/mutate/errors.d.ts +6 -0
  23. package/dist/execute/mutate/links.d.ts +38 -0
  24. package/dist/execute/mutate/plan.d.ts +5 -0
  25. package/dist/execute/mutate/runner.d.ts +101 -25
  26. package/dist/execute/mutate/single-call.d.ts +92 -0
  27. package/dist/execute/mutate/triggers.d.ts +21 -0
  28. package/dist/execute/mutate/unique-together.d.ts +5 -1
  29. package/dist/execute/mutate/validate.d.ts +11 -0
  30. package/dist/execute/transaction.d.ts +13 -2
  31. package/dist/index.d.ts +7 -2
  32. package/dist/index.js +16 -15
  33. package/dist/lora-graphql-D11QFUkf.js +17222 -0
  34. package/dist/lora-graphql-D11QFUkf.js.map +1 -0
  35. package/dist/lora-graphql.d.ts +36 -3
  36. package/dist/model/build/authorization.d.ts +11 -1
  37. package/dist/model/build/constraints.d.ts +22 -0
  38. package/dist/model/build/rule-checks.d.ts +6 -1
  39. package/dist/model/build/trigger.d.ts +9 -0
  40. package/dist/model/constraints.d.ts +112 -0
  41. package/dist/model/desugar.d.ts +7 -0
  42. package/dist/model/directives.d.ts +1 -1
  43. package/dist/model/types.d.ts +34 -0
  44. package/dist/options.d.ts +76 -3
  45. package/dist/results.d.ts +12 -1
  46. package/dist/schema/scalars.d.ts +8 -1
  47. package/dist/standard-schema.d.ts +37 -0
  48. package/dist/testing.js +1 -1
  49. package/package.json +2 -2
  50. package/dist/diff-aDivqYqr.js +0 -100
  51. package/dist/diff-aDivqYqr.js.map +0 -1
  52. package/dist/driver-DSnytFsU.js +0 -14323
  53. package/dist/driver-DSnytFsU.js.map +0 -1
@@ -7,6 +7,9 @@ import { Statistics } from './analyze/statistics.js';
7
7
  import { CompiledRead, ReadSet } from './compile/read/types.js';
8
8
  import { WriteChange } from './execute/changes.js';
9
9
  import { LoraTransaction } from './execute/transaction.js';
10
+ import { Worker, WorkerOptions } from './events/worker.js';
11
+ import { JsonSchema } from './analyze/input-schemas.js';
12
+ import { StoredReport } from './analyze/stored.js';
10
13
  import { OperationManifest } from './codegen.js';
11
14
  import { envelopPlugin } from './guards.js';
12
15
  import { GraphModel } from './model/types.js';
@@ -72,9 +75,29 @@ export declare class LoraGraphQL {
72
75
  * of caller, from the same rules as `accessMatrix()`, plus the most
73
76
  * restrictive verdict per caller. Takes a document (source or parsed)
74
77
  * or the id of a persisted operation; `operationName` picks one of
75
- * several operations.
78
+ * several operations. A mutation's inputs are read for the
79
+ * relationship writes they make (a nested connect, disconnect, create,
80
+ * update or delete), which count towards its root field's verdict.
81
+ * Pass `variables` for inputs the document takes as variables; a
82
+ * variable without a value is listed in the field's `unresolved`.
76
83
  */
77
- operationAccess(document: string | DocumentNode, operationName?: string): OperationAccess;
84
+ operationAccess(document: string | DocumentNode, operationName?: string, variables?: Readonly<Record<string, unknown>>): OperationAccess;
85
+ /**
86
+ * The validation rules as JSON Schema (2020-12), one per generated
87
+ * create and update input type, for clients: introspection does not
88
+ * show directives. Build zod, yup or valibot schemas from it, or
89
+ * validate with any JSON Schema library.
90
+ */
91
+ inputSchemas(): Record<string, JsonSchema>;
92
+ /**
93
+ * Stored values that break a validation rule of their field: rules are
94
+ * checked on write through the API, so a rule added later, or a value
95
+ * written with raw Cypher, is only found by reading. Reads at most
96
+ * `limit` nodes per type (default 10 000).
97
+ */
98
+ storedViolations(options?: {
99
+ limit?: number;
100
+ }): Promise<StoredReport>;
78
101
  /**
79
102
  * The CI gate: @cypher statements plan, the database has what the API
80
103
  * needs, and each operation's statements seek where they should.
@@ -158,6 +181,16 @@ export declare class LoraGraphQL {
158
181
  * others, and no change event fires, until `tx.commit()`.
159
182
  */
160
183
  begin(): Promise<LoraTransaction>;
161
- /** Stop the engine change feed (with `changeFeed: true`). */
184
+ /**
185
+ * Hand the nodes of an `@outbox` type to `options.handle`, after the
186
+ * commit that made them: for the side effects a transaction cannot
187
+ * hold (a push, an e-mail, a webhook). Delivery is at least once, with
188
+ * retries and backoff; work left by a restart is picked up when the
189
+ * worker starts. With `changeFeed: true` it also hears of nodes
190
+ * created outside this instance, otherwise it finds those on its next
191
+ * sweep. `close()` stops it.
192
+ */
193
+ worker(type: string, options: WorkerOptions): Worker;
194
+ /** Stop the workers, and the engine change feed (with `changeFeed: true`). */
162
195
  close(): void;
163
196
  }
@@ -1,4 +1,4 @@
1
- import { AuthOperation, Authorization, RelationshipField } from '../types.js';
1
+ import { AuthOperation, Authorization, AuthorizationWhere, NodeType, RelationshipField } from '../types.js';
2
2
  export declare function authOps(args: Record<string, unknown> | undefined): Set<AuthOperation> | undefined;
3
3
  /**
4
4
  * Field-level rules on a relationship or @cypher field: READ validate rules
@@ -28,4 +28,14 @@ export declare function relationshipRuleOperations(f: RelationshipField): Set<Au
28
28
  export declare const isRelationshipRule: (rule: {
29
29
  operations: ReadonlySet<AuthOperation>;
30
30
  }) => boolean;
31
+ /**
32
+ * The authorization of a field to an interface or union, per member: its
33
+ * relationship rules hold per member, each with that member as its
34
+ * target. Sugar expands differently per member (`target: { isViewer:
35
+ * true }` is only ever true of one), so each member gets its own copy of
36
+ * the rules, which `desugar` expands in place.
37
+ */
38
+ export declare function memberAuthorization(f: RelationshipField, nodes: ReadonlyMap<string, NodeType>, desugar: (rules: ReadonlyArray<{
39
+ where: AuthorizationWhere;
40
+ }>, target: NodeType) => void): Map<string, Authorization>;
31
41
  export declare const isRecord: (x: unknown) => x is Record<string, unknown>;
@@ -0,0 +1,22 @@
1
+ import { GraphQLDirective, GraphQLScalarType } from 'graphql';
2
+ import { FieldConstraints } from '../constraints.js';
3
+ import { ScalarType } from '../types.js';
4
+ /** The value rules of field `f`, with every problem reported through `at`. */
5
+ export declare function readConstraints(d: (n: string) => GraphQLDirective, f: {
6
+ astNode?: unknown;
7
+ extensionASTNodes?: unknown;
8
+ }, at: (message: string) => void, field: {
9
+ type: ScalarType;
10
+ list: boolean;
11
+ vector: boolean;
12
+ timestamp: boolean;
13
+ defaultValue: {
14
+ value: unknown;
15
+ } | undefined;
16
+ /** The field's custom scalar, whose rules it inherits. */
17
+ scalar?: GraphQLScalarType | undefined;
18
+ /** How a problem names the default: an argument's is not a directive. */
19
+ defaultLabel?: string | undefined;
20
+ /** Directives another reader handles (an argument's plain @size, @range). */
21
+ skip?: ReadonlySet<string> | undefined;
22
+ }): FieldConstraints | undefined;
@@ -1,5 +1,5 @@
1
1
  import { ModelProblem } from '../../errors.js';
2
- import { AbstractType, NodeType, RelationshipPropertiesType } from '../types.js';
2
+ import { AbstractType, NodeType, RelationshipField, RelationshipPropertiesType } from '../types.js';
3
3
  /** Check an `@authorization` where against the model, at startup. */
4
4
  export declare function checkAuthorizationWhere(nodes: ReadonlyMap<string, NodeType>, props: ReadonlyMap<string, RelationshipPropertiesType>, node: NodeType, where: unknown, problems: ModelProblem[], jwtShape?: ReadonlyMap<string, string>, viewerNode?: NodeType): void;
5
5
  /**
@@ -15,6 +15,11 @@ export interface RuleEnds {
15
15
  edge: RelationshipPropertiesType | undefined;
16
16
  }
17
17
  export declare function checkRuleWhere(nodes: ReadonlyMap<string, NodeType>, props: ReadonlyMap<string, RelationshipPropertiesType>, node: NodeType | undefined, type: string, field: string | undefined, where: unknown, problems: ModelProblem[], jwtShape?: ReadonlyMap<string, string>, viewerNode?: NodeType, ends?: RuleEnds, nodeless?: string): void;
18
+ /**
19
+ * Check the relationship rules of a field to an interface or union: each
20
+ * member's copy, with that member as the target.
21
+ */
22
+ export declare function checkMemberRules(nodes: ReadonlyMap<string, NodeType>, props: ReadonlyMap<string, RelationshipPropertiesType>, source: NodeType, f: RelationshipField, problems: ModelProblem[], jwtShape?: ReadonlyMap<string, string>, viewerNode?: NodeType): void;
18
23
  export declare function checkNodeWhere(nodes: ReadonlyMap<string, NodeType>, props: ReadonlyMap<string, RelationshipPropertiesType>, node: NodeType, where: unknown, path: string, at: (message: string) => void): void;
19
24
  /**
20
25
  * Interfaces and unions of a model being built, by its node map: the rule
@@ -0,0 +1,9 @@
1
+ import { GraphQLDirective, GraphQLObjectType } from 'graphql';
2
+ import { ModelProblem } from '../../errors.js';
3
+ import { NodeType, Trigger } from '../types.js';
4
+ /** What a trigger statement may read as a `$parameter`. */
5
+ export declare const TRIGGER_PARAMS: readonly string[];
6
+ /** Every `@trigger` of a node type, in the order written, checked. */
7
+ export declare function readTriggers(t: GraphQLObjectType, node: NodeType, d: (name: string) => GraphQLDirective, problems: ModelProblem[],
8
+ /** Whether the @jwt type has a @viewer claim (enables `$viewer`). */
9
+ viewer: boolean): Trigger[];
@@ -0,0 +1,112 @@
1
+ /** The rules of one field. Arguments map onto JSON Schema keywords. */
2
+ export interface FieldConstraints {
3
+ length?: {
4
+ min?: number;
5
+ max?: number;
6
+ message?: string;
7
+ };
8
+ pattern?: {
9
+ source: string;
10
+ regex: RegExp;
11
+ message?: string;
12
+ };
13
+ format?: {
14
+ name: string;
15
+ message?: string;
16
+ };
17
+ range?: {
18
+ min?: number;
19
+ max?: number;
20
+ exclusiveMin?: number;
21
+ exclusiveMax?: number;
22
+ message?: string;
23
+ };
24
+ multipleOf?: {
25
+ value: number;
26
+ message?: string;
27
+ };
28
+ size?: {
29
+ min?: number;
30
+ max?: number;
31
+ unique: boolean;
32
+ message?: string;
33
+ };
34
+ /** `@normalize`: applied before the rules, and to what is stored. */
35
+ normalize?: {
36
+ trim: boolean;
37
+ unicode?: "NFC" | "NFD" | "NFKC" | "NFKD";
38
+ case?: "LOWER" | "UPPER";
39
+ emptyToNull: boolean;
40
+ };
41
+ }
42
+ /** One rule a value breaks. `rule` is the JSON Schema keyword. */
43
+ export interface RuleFailure {
44
+ rule: string;
45
+ /** The rule's own numbers and names, for clients that localise. */
46
+ params: Record<string, unknown>;
47
+ message: string;
48
+ /** The item of a list field the failure is about. */
49
+ index?: number;
50
+ }
51
+ interface Format {
52
+ test: (value: string) => boolean;
53
+ message: string;
54
+ }
55
+ /**
56
+ * The formats `@format(as:)` knows. Named after JSON Schema's where the
57
+ * meaning matches, and differently where it does not (`url` is the WHATWG
58
+ * parser with an http/https allow-list, not RFC 3986; `e164` is a shape,
59
+ * not proof a number exists).
60
+ */
61
+ export declare const FORMATS: Readonly<Record<string, Format>>;
62
+ /**
63
+ * `value` as `@normalize` stores it: trimmed, normalised and case-mapped,
64
+ * in that order, and null for an empty string under `emptyToNull`. Items
65
+ * of a list are normalised one by one. Anything but a string is returned
66
+ * as it is.
67
+ */
68
+ export declare function normalizeValue(c: FieldConstraints, value: unknown): unknown;
69
+ /** Unicode code points in `s`. */
70
+ export declare function codePoints(s: string): number;
71
+ /**
72
+ * The rules `value` breaks, in declaration order; empty when it is valid.
73
+ * A list is checked as a list (`@size`) and then item by item.
74
+ */
75
+ export declare function checkValue(c: FieldConstraints, value: unknown): RuleFailure[];
76
+ /**
77
+ * Why `regex` is not safe to run on client input of up to `maxLength`
78
+ * characters, or undefined. JavaScript's engine backtracks, so a pattern
79
+ * such as `(a+)+` takes seconds on thirty characters. This runs the
80
+ * compiled pattern on inputs built to make it backtrack (a character it
81
+ * accepts, repeated, then one it rejects) and refuses a pattern that
82
+ * overruns the budget. A probe, not a proof: it finds the repetition
83
+ * mistakes people make, and `@length(max:)` bounds everything else.
84
+ */
85
+ export declare function probePattern(regex: RegExp, source: string, maxLength: number): string | undefined;
86
+ /**
87
+ * The rules as JSON Schema 2020-12 keywords, for one value (`item`) and
88
+ * for the list holding it (`list`). A pattern is exported anchored, since
89
+ * here it must match the whole value and in JSON Schema it need not. When
90
+ * a format and a @pattern both give an expression, they are joined with
91
+ * `allOf`.
92
+ */
93
+ export declare function jsonSchemaKeywords(c: FieldConstraints | undefined): {
94
+ item: Record<string, unknown>;
95
+ list: Record<string, unknown>;
96
+ };
97
+ /**
98
+ * The rules in a sentence or two, for the description of a generated
99
+ * input field: what a client reads in GraphiQL and in the printed schema.
100
+ */
101
+ export declare function describeConstraints(c: FieldConstraints | undefined, list: boolean): string | undefined;
102
+ /**
103
+ * What changed between two versions of a field's rules, in the terms a
104
+ * schema diff needs: `tightened` refuses input the old rules accepted,
105
+ * `changed` may, `loosened` only accepts more.
106
+ */
107
+ export declare function compareConstraints(before: FieldConstraints | undefined, after: FieldConstraints | undefined): {
108
+ tightened: string[];
109
+ changed: string[];
110
+ loosened: string[];
111
+ };
112
+ export {};
@@ -27,6 +27,13 @@ export interface DesugarContext {
27
27
  rules?: NamedRules;
28
28
  /** Named rules being expanded, to report a cycle with its chain. */
29
29
  expanding?: string[];
30
+ /**
31
+ * The member of an interface or union a relationship rule is expanded
32
+ * for, as its `target`. `isViewer` on a member that is not the viewer
33
+ * type is then never true, where elsewhere it is a mistake: the rule is
34
+ * written once for every member.
35
+ */
36
+ member?: NodeType;
30
37
  at: (message: string) => void;
31
38
  }
32
39
  /**
@@ -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 \"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 \"Default true: without a token every rule denies. false: a rule with a branch that reads no claims decides that branch for anonymous requests too; a rule that needs claims still asks for a token.\"\n requireAuthentication: Boolean\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 where: AuthorizationWhere!\n }\n\n input AuthorizationValidateRule {\n operations: [AuthOperation!]! = [READ, CREATE, UPDATE, DELETE]\n when: [AuthorizationWhen!]! = [BEFORE, AFTER]\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";
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 \"How many items a list holds, with unique refusing the same item twice. On a list field: checked on every write. On a list argument of a @cypher field: checked before the statement runs (without max, maxListArgument applies).\"\n directive @size(\n min: Int\n max: Int\n unique: Boolean\n \"Replaces the default message of this rule.\"\n message: String\n ) on ARGUMENT_DEFINITION | FIELD_DEFINITION\n\n \"Bounds of a number: min and max are inclusive, exclusiveMin and exclusiveMax are not. On an Int, Float or BigInt field (each item, for a list): checked on every write. On an argument of a @cypher field: checked before the statement runs.\"\n directive @range(\n min: Float\n max: Float\n exclusiveMin: Float\n exclusiveMax: Float\n \"Replaces the default message of this rule.\"\n message: String\n ) on ARGUMENT_DEFINITION | FIELD_DEFINITION | SCALAR\n\n \"How long a String or ID is, in Unicode code points (each item, for a list). On a custom scalar: for every field of that type.\"\n directive @length(\n min: Int\n max: Int\n \"Replaces the default message of this rule.\"\n message: String\n ) on FIELD_DEFINITION | SCALAR | ARGUMENT_DEFINITION\n\n \"An expression the whole value of a String or ID must match (each item, for a list). Needs @length(max:) on the same field.\"\n directive @pattern(\n regex: String!\n \"Replaces the default message of this rule.\"\n message: String\n ) on FIELD_DEFINITION | SCALAR | ARGUMENT_DEFINITION\n\n \"A named shape a String or ID must have: email, email-strict, url, uuid, hostname, ipv4, ipv6, slug, hex-color, ulid or e164.\"\n directive @format(\n as: String!\n \"Replaces the default message of this rule.\"\n message: String\n ) on FIELD_DEFINITION | SCALAR | ARGUMENT_DEFINITION\n\n \"Changes a String or ID before it is validated and stored (each item, for a list): trim removes surrounding whitespace, unicode normalises, case maps, in that order; emptyToNull then stores no value for an empty string. Opt in: nothing is changed without it.\"\n directive @normalize(\n trim: Boolean\n case: TextCase\n unicode: UnicodeForm\n emptyToNull: Boolean\n ) on FIELD_DEFINITION | SCALAR\n\n enum TextCase {\n LOWER\n UPPER\n }\n\n enum UnicodeForm {\n NFC\n NFD\n NFKC\n NFKD\n }\n\n \"An Int or Float must be a whole multiple of value (each item, for a list).\"\n directive @multipleOf(\n value: Float!\n \"Replaces the default message of this rule.\"\n message: String\n ) on FIELD_DEFINITION | SCALAR | 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 \"A Cypher statement run inside the transaction of every generated mutation that creates, updates or deletes nodes of this type, before the commit. this is each written node; $jwt, $viewer and $operation are bound. Nodes it returns as created or updated join the mutation's write-set. Raw Cypher and @cypher mutations fire no trigger.\"\n directive @trigger(\n on: [MutationOperation!]!\n statement: String!\n \"Fire an UPDATE trigger only for nodes whose update set one of these fields.\"\n fields: [String!]\n ) repeatable on OBJECT\n\n \"Nodes of this type are work for lora.worker(): each one created by a generated mutation or returned as created by a @trigger is handed to the worker's handler after the commit, at least once, with retries. The delivery state is stored on the node, outside the API.\"\n directive @outbox 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(\n statement: String!\n columnName: String\n \"Rows the statement examines each time it runs, charged against maxCost: once for a root field, once per parent node for a field of a type. Without it only the rows the field returns are charged.\"\n cost: Int\n ) 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 \"Default true: without a token every rule denies. false: a rule with a branch that reads no claims decides that branch for anonymous requests too; a rule that needs claims still asks for a token.\"\n requireAuthentication: Boolean\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 where: AuthorizationWhere!\n }\n\n input AuthorizationValidateRule {\n operations: [AuthOperation!]! = [READ, CREATE, UPDATE, DELETE]\n when: [AuthorizationWhen!]! = [BEFORE, AFTER]\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>;
@@ -1,9 +1,26 @@
1
+ import { FieldConstraints } from './constraints.js';
1
2
  export type ScalarType = "String" | "ID" | "Int" | "Float" | "Boolean" | "BigInt" | "Date" | "Time" | "LocalTime" | "DateTime" | "LocalDateTime" | "Duration" | "Point" | "CartesianPoint"
2
3
  /** A user-declared enum, stored as its value name. */
3
4
  | "Enum";
4
5
  export type FilterOperator = "EQ" | "IN" | "LT" | "LTE" | "GT" | "GTE" | "CONTAINS" | "STARTS_WITH" | "ENDS_WITH" | "WITHIN_BBOX" | "DISTANCE" | "INCLUDES" | "IS_NULL" | "CASE_INSENSITIVE";
5
6
  export type IndexKind = "RANGE" | "TEXT" | "POINT";
6
7
  export type MutationOperation = "CREATE" | "UPDATE" | "DELETE";
8
+ /**
9
+ * `@trigger(on:, statement:, fields:)`: a statement run for the nodes of
10
+ * a type that a generated mutation wrote, inside its transaction.
11
+ */
12
+ export interface Trigger {
13
+ operations: ReadonlySet<MutationOperation>;
14
+ statement: string;
15
+ /** `UPDATE` fires only for nodes whose update set one of these. */
16
+ fields: readonly string[] | undefined;
17
+ /** The `$parameters` the statement uses: `jwt`, `viewer`, `operation`. */
18
+ params: readonly string[];
19
+ /** Whether the statement has a write clause. */
20
+ writes: boolean;
21
+ /** Whether it returns a `created` or `updated` column. */
22
+ reports: boolean;
23
+ }
7
24
  export type AuthOperation = "READ" | MutationOperation | "CREATE_RELATIONSHIP" | "DELETE_RELATIONSHIP" | "SUBSCRIBE" | RelationshipOperation;
8
25
  /** Operations of rules on a relationship field, over `source`, `target`, `edge`. */
9
26
  export type RelationshipOperation = "CONNECT" | "DISCONNECT" | "UPDATE_EDGE" | "READ_EDGE";
@@ -53,6 +70,8 @@ export interface ScalarField extends FieldBase {
53
70
  defaultValue: {
54
71
  value: unknown;
55
72
  } | undefined;
73
+ /** `@length`, `@pattern`, `@format`, `@range`, `@multipleOf`, `@size`. */
74
+ constraints?: FieldConstraints | undefined;
56
75
  /** `@timestamp`: set to the current time by these operations. */
57
76
  timestamp: ReadonlySet<"CREATE" | "UPDATE"> | undefined;
58
77
  /** Never client-settable (`@readonly`, `@timestamp`, `@private`). */
@@ -110,6 +129,13 @@ export interface RelationshipField extends FieldBase {
110
129
  members: readonly string[];
111
130
  /** For a member copy of a polymorphic field: the field it came from. */
112
131
  via?: RelationshipField | undefined;
132
+ /**
133
+ * For a field to an interface or union with relationship rules (CONNECT,
134
+ * DISCONNECT, UPDATE_EDGE, READ_EDGE): the field's authorization per
135
+ * member, its rules expanded and checked with that member as their
136
+ * `target`. A member copy carries its own.
137
+ */
138
+ memberAuthorization?: ReadonlyMap<string, Authorization> | undefined;
113
139
  filterable: boolean;
114
140
  /** UNDIRECTED: reads follow the relationship both ways. */
115
141
  queryDirection: "DIRECTED" | "UNDIRECTED";
@@ -151,6 +177,8 @@ export interface CypherArgument {
151
177
  min?: number | undefined;
152
178
  max?: number | undefined;
153
179
  } | undefined;
180
+ /** The other value rules of the argument, as on a field. */
181
+ constraints?: FieldConstraints | undefined;
154
182
  }
155
183
  export interface CypherField extends FieldBase {
156
184
  kind: "cypher";
@@ -160,6 +188,8 @@ export interface CypherField extends FieldBase {
160
188
  statement: string;
161
189
  /** The result column holding the field's value. */
162
190
  columnName: string;
191
+ /** `@cypher(cost:)`: rows the statement examines per run; 0 without. */
192
+ cost: number;
163
193
  /** The field's type; `node` names a @node type when it returns nodes. */
164
194
  type: TypeShape;
165
195
  node: string | undefined;
@@ -281,6 +311,10 @@ export interface NodeType {
281
311
  interfaces: readonly string[];
282
312
  /** `@uniqueTogether`: combinations no two nodes of the type may share. */
283
313
  uniqueTogether: readonly UniqueTogether[];
314
+ /** `@outbox`: nodes of the type are work for `lora.worker()`. */
315
+ outbox: boolean;
316
+ /** `@trigger`: statements run in the transaction of a mutation, in SDL order. */
317
+ triggers: readonly Trigger[];
284
318
  description: string | undefined;
285
319
  }
286
320
  /**
package/dist/options.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { DocumentNode, GraphQLFieldResolver, GraphQLScalarType } from 'graphql';
2
2
  import { LoraDriver, Statement } from './driver.js';
3
- import { PopulatedByCallback } from './execute/mutate/env.js';
3
+ import { PopulatedByCallback, TriggerFunctions } from './execute/mutate/env.js';
4
4
  import { LoraTransaction } from './execute/transaction.js';
5
5
  import { DocumentGuards } from './guards.js';
6
6
  import { ModelOptions } from './model/build/limits.js';
7
+ import { TypeValidator } from './standard-schema.js';
7
8
  import { ObservabilityOptions } from './observe.js';
8
9
  export interface LoraGraphQLOptions extends ModelOptions, ObservabilityOptions {
9
10
  /** Annotated SDL: the graph model and the API in one document. */
@@ -61,6 +62,13 @@ export interface LoraGraphQLOptions extends ModelOptions, ObservabilityOptions {
61
62
  * request. Also the default `limit` of bulk updates and deletes.
62
63
  */
63
64
  maxBatch?: number;
65
+ /**
66
+ * Write transactions that may wait for the writer lock at once. One
67
+ * more fails at once with OVERLOADED (answer it with HTTP 503 and let
68
+ * the client back off) instead of joining a queue whose wait only
69
+ * grows: one writer runs at a time. Default 512; `Infinity` disables.
70
+ */
71
+ maxQueuedWrites?: number;
64
72
  /**
65
73
  * Most items a list argument of a `@cypher` field takes, unless its
66
74
  * `@size(max:)` says otherwise. More is BAD_USER_INPUT before the
@@ -85,6 +93,15 @@ export interface LoraGraphQLOptions extends ModelOptions, ObservabilityOptions {
85
93
  * may hold. More is BAD_USER_INPUT before anything runs. Default 10 000.
86
94
  */
87
95
  maxStringFilter?: number;
96
+ /**
97
+ * Most matches the `totalCount` of a root or search connection counts.
98
+ * A count stops there, so `totalCount` equal to this means "this many
99
+ * or more": enough for "1000+ results", at a bounded cost on a label
100
+ * of any size. Aggregates, and the `totalCount` of a connection nested
101
+ * under a node (bounded by that node's relationships), are not capped.
102
+ * Default: no cap.
103
+ */
104
+ maxTotalCount?: number;
88
105
  /**
89
106
  * Changes a `changes()` consumer or subscriber may fall behind before it
90
107
  * is ended with an error. Default 1000.
@@ -113,6 +130,50 @@ export interface LoraGraphQLOptions extends ModelOptions, ObservabilityOptions {
113
130
  subscriptionTimeoutMs?: number;
114
131
  /** Named callbacks for `@populatedBy(callback:)`. */
115
132
  callbacks?: Record<string, PopulatedByCallback>;
133
+ /**
134
+ * Send a mutation whose statements are all known up front (today: a
135
+ * create with the nodes it creates and connects, or an update of one
136
+ * node by key that sets or adjusts scalar fields and connects or
137
+ * creates related nodes, on a schema without
138
+ * triggers) as
139
+ * one call that begins, runs and commits, with its rule checks as
140
+ * `assert()` statements: the writer lock is then never held across a
141
+ * hop to the JavaScript thread. Needs a LoraDB with `assert()`; asked
142
+ * once, and without it every mutation keeps its transaction. Default
143
+ * true; false keeps every mutation in a transaction.
144
+ */
145
+ singleCallMutations?: boolean;
146
+ /**
147
+ * Trigger functions per node type: `{ Comment: { create, update,
148
+ * delete } }`. Each is called inside the transaction of a generated
149
+ * mutation that wrote nodes of the type, after the type's `@trigger`
150
+ * statements and before the commit, with the keys written, the
151
+ * write-set so far and the transaction. Throwing rolls the mutation
152
+ * back: a `GraphQLError` reaches the client as thrown, anything else
153
+ * is reported like a database error. Mutations a trigger runs in its
154
+ * transaction fire no trigger. A trigger holds the database's writer
155
+ * lock: keep network calls out of it.
156
+ */
157
+ triggers?: Record<string, TriggerFunctions>;
158
+ /**
159
+ * Most time one trigger function may take, in milliseconds; past it
160
+ * the mutation fails with TIMEOUT and rolls back. Default 1000; 0
161
+ * disables.
162
+ */
163
+ triggerTimeoutMs?: number;
164
+ /**
165
+ * A validator per node type, for what the validation directives do not
166
+ * say (a rule across two fields, a check against your own data): any
167
+ * Standard Schema object, which zod, valibot, arktype and yup schemas
168
+ * are. It is given each input of that type (a create's, an update's, a
169
+ * nested one's) after `@normalize` and after the field rules hold, and
170
+ * its issues are reported like theirs, in `extensions.violations` with
171
+ * `rule: "custom"`. What it returns is what is written, so its
172
+ * transforms apply. One schema validates every input; `{ create,
173
+ * update, upsert }` gives each kind its own, which matters because an
174
+ * update's input holds only the fields it sets.
175
+ */
176
+ validators?: Record<string, TypeValidator>;
116
177
  /**
117
178
  * Implementations of custom scalars declared with `@storedAs`, e.g. one
118
179
  * that validates e-mail addresses. Without one a scalar passes through.
@@ -190,8 +251,11 @@ export interface DatabaseErrorEvent {
190
251
  /** Correlation id, also in the client error's `extensions.id`. */
191
252
  id: string;
192
253
  /**
193
- * The root field that failed, or `changeFeed` for a failure of the
194
- * change feed (it reopens from where it stopped after a backoff).
254
+ * The root field that failed, `changeFeed` for a failure of the
255
+ * change feed (it reopens from where it stopped after a backoff), or
256
+ * `worker:<Type>` for a worker: a node given up after `maxAttempts`
257
+ * (with the handler's last error), or a failed read or write of its
258
+ * own.
195
259
  */
196
260
  field: string;
197
261
  /** The engine's message, which may name labels, properties and Cypher. */
@@ -231,6 +295,15 @@ export interface CheckOptions {
231
295
  * the GraphQL context to compile them under (e.g. `{ jwt }`, so rules
232
296
  * compile as they do for a signed-in caller). Without one, an operation
233
297
  * compiles as an anonymous request would.
298
+ *
299
+ * A query is compiled, not run. A mutation is run, because what it
300
+ * does depends on what it finds, in a transaction that is always rolled
301
+ * back: nothing is written and no change is reported, but its
302
+ * `@populatedBy` callbacks and trigger functions are called. Each
303
+ * statement it ran, a `@trigger`'s included, must start from keys: one
304
+ * that expands from a full scan is a finding. A mutation that fails is
305
+ * an error of the check, so give it variables that succeed against the
306
+ * database being checked.
234
307
  */
235
308
  operations?: Array<{
236
309
  name?: string;
package/dist/results.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { StoredViolation } from './analyze/stored.js';
1
2
  import { ExecutionResult } from 'graphql';
2
3
  import { CypherFinding } from './analyze/cypher-check.js';
3
4
  import { SchemaRequirement } from './analyze/indexes.js';
@@ -8,7 +9,11 @@ import { ModelWarning } from './model/types.js';
8
9
  export interface ExecutionTiming {
9
10
  /** The whole `execute()` call: parse, validation, execution. */
10
11
  totalMs: number;
11
- /** Time spent running statements in LoraDB. */
12
+ /**
13
+ * Time spent running statements in LoraDB, summed over the root fields.
14
+ * A query's root fields run concurrently, so this can exceed `totalMs`;
15
+ * within one field it never exceeds that field's `totalMs`.
16
+ */
12
17
  databaseMs: number;
13
18
  /** Per root field, by response key (alias or name). */
14
19
  fields: Record<string, {
@@ -67,4 +72,10 @@ export interface CheckReport {
67
72
  operation: string;
68
73
  message: string;
69
74
  }>;
75
+ /**
76
+ * Stored values that break a validation rule of their field (not a
77
+ * failure): a rule added or tightened after they were written, or a
78
+ * value written with raw Cypher. At most 10 000 nodes per type are read.
79
+ */
80
+ stored: StoredViolation[];
70
81
  }
@@ -1,4 +1,11 @@
1
- import { GraphQLScalarType } from 'graphql';
1
+ import { GraphQLError, GraphQLScalarType, ValueNode } from 'graphql';
2
+ /**
3
+ * A value this scalar refuses. Coded here, so that it is BAD_USER_INPUT
4
+ * whichever way it arrived: as a variable, as a literal in the document
5
+ * (which fails GraphQL validation, before any resolver), through
6
+ * `execute()` or through another server on `getSchema()`.
7
+ */
8
+ export declare function badInput(message: string, nodes?: ValueNode): GraphQLError;
2
9
  export declare const GraphQLDate: GraphQLScalarType<unknown, unknown>;
3
10
  export declare const GraphQLTime: GraphQLScalarType<unknown, unknown>;
4
11
  export declare const GraphQLLocalTime: GraphQLScalarType<unknown, unknown>;
@@ -0,0 +1,37 @@
1
+ /** A validator from any library that implements Standard Schema v1. */
2
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
3
+ readonly "~standard": {
4
+ readonly version: 1;
5
+ readonly vendor: string;
6
+ readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
7
+ readonly types?: {
8
+ readonly input: Input;
9
+ readonly output: Output;
10
+ } | undefined;
11
+ };
12
+ }
13
+ export type StandardResult<Output> = {
14
+ readonly value: Output;
15
+ readonly issues?: undefined;
16
+ } | {
17
+ readonly issues: ReadonlyArray<StandardIssue>;
18
+ };
19
+ export interface StandardIssue {
20
+ readonly message: string;
21
+ /** Keys into the value; a segment is a key, or an object holding one. */
22
+ readonly path?: ReadonlyArray<PropertyKey | {
23
+ readonly key: PropertyKey;
24
+ }> | undefined;
25
+ }
26
+ /**
27
+ * The validator of one node type's mutation inputs: one schema for every
28
+ * input, or one per kind of input. An update's input holds only the
29
+ * fields it sets, and an upsert's only the key for certain, so a schema
30
+ * that requires fields belongs under `create`.
31
+ */
32
+ export type TypeValidator = StandardSchemaV1 | {
33
+ create?: StandardSchemaV1;
34
+ update?: StandardSchemaV1;
35
+ upsert?: StandardSchemaV1;
36
+ };
37
+ export declare function isStandardSchema(value: unknown): value is StandardSchemaV1;
package/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { graphql as g } from "graphql";
2
- import { a as h, l as b } from "./driver-DSnytFsU.js";
2
+ import { a as h, l as b } from "./lora-graphql-D11QFUkf.js";
3
3
  async function T(l) {
4
4
  let u;
5
5
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loradb/lora-graphql",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Schema-first GraphQL for LoraDB: one annotated SDL becomes an executable, index-aware GraphQL API that compiles every operation to a single Cypher statement.",
5
5
  "license": "BUSL-1.1",
6
6
  "packageManager": "yarn@4.5.1",
@@ -65,7 +65,7 @@
65
65
  "bench:load": "node bench/load/main.mjs"
66
66
  },
67
67
  "peerDependencies": {
68
- "@loradb/lora-node": "^0.24.0",
68
+ "@loradb/lora-node": "^0.25.0",
69
69
  "graphql": "^16.8.0 || ^17.0.0"
70
70
  },
71
71
  "devDependencies": {