@loradb/lora-graphql 0.19.0 → 0.20.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/index.d.ts CHANGED
@@ -6,6 +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
10
  export type { CypherFinding } from './analyze/cypher-check.js';
10
11
  export { diffSchemas, type ApiChange, type SchemaDiff, } from './analyze/diff.js';
11
12
  export type { CompiledRead, ReadSet, SeekExpectation } from './compile/read.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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-CuXOoYWT.js";
2
- import { d as x } from "./diff-BH4Na9pC.js";
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-C7a5NkfH.js";
2
+ import { d as x } from "./diff-DUOLUgrI.js";
3
3
  export {
4
4
  e as DEFAULT_GUARDS,
5
5
  r as LoraGraphQL,
@@ -2,6 +2,7 @@ import { DocumentNode, ExecutionResult, GraphQLSchema, GraphQLFieldResolver, Gra
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
6
  import { Statistics } from './analyze/statistics.js';
6
7
  import { CompiledRead, ReadSet } from './compile/read.js';
7
8
  import { LoraDriver, Statement } from './driver.js';
@@ -92,6 +93,17 @@ export interface LoraGraphQLOptions extends ModelOptions, ObservabilityOptions {
92
93
  * persisted operations by `id`. Default false.
93
94
  */
94
95
  persistedOnly?: boolean;
96
+ /**
97
+ * How `execute()` commits a mutation with several root fields.
98
+ * `"field"` (the default): each root field in its own transaction, so a
99
+ * later failure leaves the earlier ones committed. `"operation"`: every
100
+ * root field in one transaction, committed only when the operation
101
+ * 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.
105
+ */
106
+ mutationTransaction?: "field" | "operation";
95
107
  }
96
108
  export interface CostEvent {
97
109
  field: string;
@@ -179,6 +191,11 @@ export interface CheckReport {
179
191
  }>;
180
192
  /** Schema lint: valid but costly or risky choices (not failures). */
181
193
  lint: readonly ModelWarning[];
194
+ /**
195
+ * `@mutation` types with writes no rule guards (failures): declare them
196
+ * with `@authorization(public: [...])` when that is intended.
197
+ */
198
+ security: readonly ModelWarning[];
182
199
  /** Plan findings per operation and root field. */
183
200
  plans: Array<{
184
201
  operation: string;
@@ -233,6 +250,13 @@ export declare class LoraGraphQL {
233
250
  * The CI gate: @cypher statements plan, the database has what the API
234
251
  * needs, and each operation's statements seek where they should.
235
252
  */
253
+ /**
254
+ * Who may do what: for every type and guarded field, each operation as
255
+ * each kind of caller (anonymous, authenticated, and each role the rules
256
+ * test), with the verdict and the rules that decide it. Read off the
257
+ * model; stable, so it can be snapshotted and reviewed as a diff.
258
+ */
259
+ accessMatrix(): AccessEntry[];
236
260
  check(options?: CheckOptions): Promise<CheckReport>;
237
261
  /**
238
262
  * Compile a query without running it: one entry per root field, with its
@@ -0,0 +1,34 @@
1
+ import { ModelProblem } from '../errors.js';
2
+ import { AuthorizationWhere, NodeType, RelationshipPropertiesType } from './types.js';
3
+ /** The `@viewer` claim: which node, by which unique field, is the caller. */
4
+ export interface ViewerMapping {
5
+ claim: string;
6
+ type: string;
7
+ field: string;
8
+ }
9
+ /** Named rules: claims-only schema rules, and rules per node type. */
10
+ export interface NamedRules {
11
+ schema: ReadonlyMap<string, AuthorizationWhere>;
12
+ byType: ReadonlyMap<string, ReadonlyMap<string, AuthorizationWhere>>;
13
+ }
14
+ export interface DesugarContext {
15
+ nodes: ReadonlyMap<string, NodeType>;
16
+ props: ReadonlyMap<string, RelationshipPropertiesType>;
17
+ viewer: ViewerMapping | undefined;
18
+ rules?: NamedRules;
19
+ /** Named rules being expanded, to report a cycle with its chain. */
20
+ expanding?: string[];
21
+ at: (message: string) => void;
22
+ }
23
+ /**
24
+ * A rule's where, sugar expanded. `owner` is the type the rule's `node`
25
+ * part tests (undefined on relationship properties: claims only).
26
+ */
27
+ export declare function desugarRule(ctx: DesugarContext, owner: NodeType | undefined, where: AuthorizationWhere, ends?: {
28
+ source: NodeType;
29
+ target: NodeType;
30
+ }): AuthorizationWhere;
31
+ /** A `node` filter over `node`, sugar expanded, keys kept in order. */
32
+ export declare function desugarNode(ctx: DesugarContext, node: NodeType, where: unknown): unknown;
33
+ /** Model problems for a `@viewer` mapping, once the node types are built. */
34
+ export declare function checkViewer(viewer: ViewerMapping, nodes: ReadonlyMap<string, NodeType>, jwtType: string, problems: ModelProblem[]): void;
@@ -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(generate: Boolean = false) on FIELD_DEFINITION\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 \"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 OBJECT | FIELD_DEFINITION\n\n input AuthorizationFilterRule {\n operations: [AuthOperation!]! = [READ, UPDATE, DELETE]\n requireAuthentication: Boolean = true\n where: AuthorizationWhere!\n }\n\n input AuthorizationValidateRule {\n operations: [AuthOperation!]! = [READ, CREATE, UPDATE, DELETE]\n when: [AuthorizationWhen!]! = [BEFORE, AFTER]\n requireAuthentication: Boolean = true\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 }\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 \"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";
2
2
  /** Names defined by the prelude; never treated as user types. */
3
3
  export declare const PRELUDE_TYPES: Set<string>;
@@ -0,0 +1,22 @@
1
+ import { GraphQLNamedType, GraphQLSchema } from 'graphql';
2
+ import { ModelProblem } from '../errors.js';
3
+ export type Position = "node type" | "relationship properties type" | "@jwt type" | "object type without @node" | "Query or Mutation type" | "interface" | "union" | "node field" | "relationship field" | "@cypher field" | "Query or Mutation field" | "@customResolver field" | "relationship property" | "interface field" | "interface relationship field" | "field of an object type without @node" | "@jwt claim";
4
+ /**
5
+ * The directives each position applies, plus those it refuses with a more
6
+ * specific message elsewhere in the model build (listed so they are not
7
+ * reported twice).
8
+ */
9
+ export declare const DIRECTIVE_POSITIONS: Readonly<Record<Position, readonly string[]>>;
10
+ /** The directives this library defines (not graphql's built-ins). */
11
+ export declare function libraryDirectives(): ReadonlySet<string>;
12
+ export interface PositionContext {
13
+ schema: GraphQLSchema;
14
+ nodeNames: ReadonlySet<string>;
15
+ propsNames: ReadonlySet<string>;
16
+ jwtType: string | undefined;
17
+ /** @node types, interfaces and unions: what a relationship may target. */
18
+ targetNames: ReadonlySet<string>;
19
+ userTypes: readonly GraphQLNamedType[];
20
+ }
21
+ /** Report every library directive used where the model would ignore it. */
22
+ export declare function checkDirectivePositions(ctx: PositionContext, problems: ModelProblem[]): void;
@@ -4,7 +4,10 @@ export type ScalarType = "String" | "ID" | "Int" | "Float" | "Boolean" | "BigInt
4
4
  export type FilterOperator = "EQ" | "IN" | "LT" | "LTE" | "GT" | "GTE" | "CONTAINS" | "STARTS_WITH" | "ENDS_WITH" | "WITHIN_BBOX" | "DISTANCE" | "INCLUDES" | "IS_NULL" | "CASE_INSENSITIVE";
5
5
  export type IndexKind = "RANGE" | "TEXT" | "POINT";
6
6
  export type MutationOperation = "CREATE" | "UPDATE" | "DELETE";
7
- export type AuthOperation = "READ" | MutationOperation | "CREATE_RELATIONSHIP" | "DELETE_RELATIONSHIP" | "SUBSCRIBE";
7
+ export type AuthOperation = "READ" | MutationOperation | "CREATE_RELATIONSHIP" | "DELETE_RELATIONSHIP" | "SUBSCRIBE" | RelationshipOperation;
8
+ /** Operations of rules on a relationship field, over `source`, `target`, `edge`. */
9
+ export type RelationshipOperation = "CONNECT" | "DISCONNECT" | "UPDATE_EDGE" | "READ_EDGE";
10
+ export declare const RELATIONSHIP_OPERATIONS: ReadonlySet<AuthOperation>;
8
11
  /** Field-level options shared by every field kind. */
9
12
  interface FieldBase {
10
13
  /** Operations on which reading this field needs an authenticated request. */
@@ -39,6 +42,13 @@ export interface ScalarField extends FieldBase {
39
42
  indexes: readonly IndexKind[];
40
43
  /** `@key(generate: true)`: creates fill a UUID when the input omits it. */
41
44
  generate: boolean;
45
+ /**
46
+ * `@key(scope: VIEWER)`: a created key starts with the caller's `@viewer`
47
+ * claim and `separator`.
48
+ */
49
+ keyScope?: {
50
+ separator: string;
51
+ } | undefined;
42
52
  /** `@default(value:)`, stored on create when the input omits the field. */
43
53
  defaultValue: {
44
54
  value: unknown;
@@ -177,17 +187,33 @@ export type AuthorizationWhere = Record<string, unknown>;
177
187
  export interface AuthorizationFilterRule {
178
188
  operations: ReadonlySet<AuthOperation>;
179
189
  requireAuthentication: boolean;
190
+ /** `requireAuthentication` was left to its default (true). */
191
+ requireAuthenticationDefaulted?: boolean;
180
192
  where: AuthorizationWhere;
181
193
  }
182
194
  export interface AuthorizationValidateRule {
183
195
  operations: ReadonlySet<AuthOperation>;
184
196
  when: ReadonlySet<"BEFORE" | "AFTER">;
185
197
  requireAuthentication: boolean;
198
+ /** `requireAuthentication` was left to its default (true). */
199
+ requireAuthenticationDefaulted?: boolean;
186
200
  where: AuthorizationWhere;
187
201
  }
188
202
  export interface Authorization {
189
203
  filter: readonly AuthorizationFilterRule[];
190
204
  validate: readonly AuthorizationValidateRule[];
205
+ /** `bypass: false`: the schema's bypass does not skip these rules. */
206
+ bypass?: boolean;
207
+ /** `public:` operations deliberately open to every caller. */
208
+ public?: ReadonlySet<AuthOperation>;
209
+ /**
210
+ * `mask:` on a scalar field: a row failing `unless` reads the field as
211
+ * `value`, in projections and filters alike.
212
+ */
213
+ mask?: ReadonlyArray<{
214
+ unless: AuthorizationWhere;
215
+ value: unknown;
216
+ }>;
191
217
  }
192
218
  export interface PageLimit {
193
219
  default: number;
@@ -307,6 +333,17 @@ export interface GraphModel {
307
333
  objects: ReadonlyMap<string, PlainObjectType>;
308
334
  /** The `@jwt` claims shape, when declared: claim name → token path. */
309
335
  jwt: ReadonlyMap<string, string> | undefined;
336
+ /**
337
+ * `@authorizationDefaults(bypass:)`: a claims-only test that, when a
338
+ * request passes it, skips every filter and validate rule.
339
+ */
340
+ bypass: AuthorizationWhere | undefined;
341
+ /** The `@viewer` claim: the caller's node type and identifying field. */
342
+ viewer: {
343
+ claim: string;
344
+ type: string;
345
+ field: string;
346
+ } | undefined;
310
347
  /** Secret cursors are signed with, when configured. */
311
348
  cursorSecret: string | undefined;
312
349
  }
package/dist/testing.d.ts CHANGED
@@ -34,3 +34,30 @@ export declare function expectSeeks(lora: LoraGraphQL, document: string | Docume
34
34
  rowBudget?: number;
35
35
  context?: unknown;
36
36
  }): Promise<void>;
37
+ /** The access `expectAccess` expects: entries allowed, and entries denied. */
38
+ export interface AccessExpectations {
39
+ /** The claims to act as; undefined acts anonymously. */
40
+ as: Record<string, unknown> | undefined;
41
+ /** Other GraphQL context values the rules read (`$context.*`). */
42
+ context?: Record<string, unknown>;
43
+ allowed?: readonly string[];
44
+ denied?: readonly string[];
45
+ }
46
+ /**
47
+ * Assert who may do what, against the database: each entry runs as the
48
+ * caller in a transaction that is always rolled back, and every mismatch
49
+ * is reported at once. Entries:
50
+ *
51
+ * - `read Trip lou:tomorrowland` (denied: not visible, or refused)
52
+ * - `create Trip lou:x {"name": "X"}` (other input fields as JSON)
53
+ * - `update Trip lou:x {"name": "Y"}` and `delete Trip lou:x`
54
+ * - `connect Trip.members lou:tomorrowland → f1` (also `->`)
55
+ * - `disconnect Trip.members lou:tomorrowland → f1`
56
+ * - `update-edge Trip.members lou:tomorrowland → f1 {"rsvp": "GOING"}`
57
+ *
58
+ * Denied means `FORBIDDEN`, `UNAUTHENTICATED` or `NOT_FOUND` (a node the
59
+ * caller cannot see); any other error is reported as a mismatch either way.
60
+ */
61
+ export declare function expectAccess(target: LoraGraphQL | {
62
+ lora: LoraGraphQL;
63
+ }, expectations: AccessExpectations): Promise<void>;
package/dist/testing.js CHANGED
@@ -1,56 +1,173 @@
1
1
  import { graphql as h } from "graphql";
2
- import { L as w, l as u } from "./driver-CuXOoYWT.js";
3
- async function L(l) {
4
- let s;
2
+ import { L as w, l as g } from "./driver-C7a5NkfH.js";
3
+ async function _(d) {
4
+ let l;
5
5
  try {
6
- ({ createDatabase: s } = await import("@loradb/lora-node"));
6
+ ({ createDatabase: l } = await import("@loradb/lora-node"));
7
7
  } catch {
8
8
  throw new Error(
9
9
  "createTestLoraGraphQL needs @loradb/lora-node: npm install --save-dev @loradb/lora-node"
10
10
  );
11
11
  }
12
- const t = await s(), n = [], { seed: a, onStatement: i, ...c } = l, r = new w({
12
+ const i = await l(), e = [], { seed: t, onStatement: a, ...c } = d, n = new w({
13
13
  ...c,
14
- driver: u(t),
15
- onStatement: (e) => {
16
- n.push(e), i?.(e);
14
+ driver: g(i),
15
+ onStatement: (r) => {
16
+ e.push(r), a?.(r);
17
17
  }
18
18
  });
19
- if (await r.assertSchema({ create: !0 }), typeof a == "function") await a(t);
19
+ if (await n.assertSchema({ create: !0 }), typeof t == "function") await t(i);
20
20
  else
21
- for (const e of typeof a == "string" ? [a] : a ?? [])
22
- await t.execute(e);
23
- const o = r.getSchema(), m = (e, p = {}, d = {}) => h({ schema: o, source: e, variableValues: p, contextValue: d });
21
+ for (const r of typeof t == "string" ? [t] : t ?? [])
22
+ await i.execute(r);
23
+ const o = n.getSchema(), s = (r, u = {}, p = {}) => h({ schema: o, source: r, variableValues: u, contextValue: p });
24
24
  return {
25
- lora: r,
26
- db: t,
25
+ lora: n,
26
+ db: i,
27
27
  schema: o,
28
- statements: n,
29
- run: m,
30
- data: async (e, p, d) => {
31
- const f = await m(e, p, d);
32
- if (f.errors) throw f.errors[0];
33
- return f.data;
28
+ statements: e,
29
+ run: s,
30
+ data: async (r, u, p) => {
31
+ const $ = await s(r, u, p);
32
+ if ($.errors) throw $.errors[0];
33
+ return $.data;
34
34
  },
35
- close: () => t.dispose?.()
35
+ close: () => i.dispose?.()
36
36
  };
37
37
  }
38
- async function b(l, s, t = {}, n = {}) {
39
- const i = (await l.explain(s, t, n)).flatMap(
38
+ async function N(d, l, i = {}, e = {}) {
39
+ const a = (await d.explain(l, i, e)).flatMap(
40
40
  (c) => c.reports.flatMap(
41
- (r) => r.findings.map((o) => `${c.field}: ${o.message}
41
+ (n) => n.findings.map((o) => `${c.field}: ${o.message}
42
42
  ${o.statement}`)
43
43
  )
44
44
  );
45
- if (i.length > 0)
45
+ if (a.length > 0)
46
46
  throw new Error(
47
47
  `expected every root field to seek:
48
- ${i.join(`
48
+ ${a.join(`
49
49
  `)}`
50
50
  );
51
51
  }
52
+ async function x(d, l) {
53
+ const i = d instanceof w ? d : d.lora, e = [], t = async (a, c) => {
54
+ let n;
55
+ try {
56
+ n = await b(i, a, l);
57
+ } catch (o) {
58
+ e.push(
59
+ `${a}: ${o instanceof Error ? o.message : String(o)}`
60
+ );
61
+ return;
62
+ }
63
+ n.kind === "error" ? e.push(`${a}: failed with ${n.message}`) : c && n.kind === "denied" ? e.push(
64
+ `expected allowed, was denied: ${a} (${n.message})`
65
+ ) : !c && n.kind === "allowed" && e.push(`expected denied, was allowed: ${a}`);
66
+ };
67
+ for (const a of l.allowed ?? []) await t(a, !0);
68
+ for (const a of l.denied ?? []) await t(a, !1);
69
+ if (e.length > 0) {
70
+ const a = l.as ? JSON.stringify(l.as) : "anonymous";
71
+ throw new Error(
72
+ `access as ${a} differs in ${e.length} ${e.length === 1 ? "entry" : "entries"}:
73
+ ${e.join(`
74
+ `)}`
75
+ );
76
+ }
77
+ }
78
+ const v = /* @__PURE__ */ new Set(["FORBIDDEN", "UNAUTHENTICATED", "NOT_FOUND"]);
79
+ async function b(d, l, i) {
80
+ const { source: e, variables: t, isRead: a, root: c } = E(d, l), n = await d.begin();
81
+ try {
82
+ const o = await d.execute({
83
+ source: e,
84
+ variables: t,
85
+ context: {
86
+ ...i.context,
87
+ ...i.as ? { jwt: i.as } : {},
88
+ transaction: n
89
+ }
90
+ }), s = o.errors?.[0];
91
+ if (s) {
92
+ const r = s.extensions?.code;
93
+ return typeof r == "string" && v.has(r) ? { kind: "denied", message: `${r}: ${s.message}` } : {
94
+ kind: "error",
95
+ message: `${String(r ?? "error")}: ${s.message}`
96
+ };
97
+ }
98
+ return a && o.data?.[c] == null ? { kind: "denied", message: "not visible" } : { kind: "allowed" };
99
+ } finally {
100
+ await n.rollback();
101
+ }
102
+ }
103
+ function E(d, l) {
104
+ const i = /^\s*(read|create|update|delete|connect|disconnect|update-edge)\s+([A-Za-z_][\w]*)(?:\.([A-Za-z_]\w*))?\s+(\S+)(?:\s*(?:→|->)\s*(\S+))?\s*(\{.*\})?\s*$/s.exec(
105
+ l
106
+ );
107
+ if (!i)
108
+ throw new Error(
109
+ "expected `<read|create|update|delete> Type key [json]` or `<connect|disconnect|update-edge> Type.field key → key [json]`"
110
+ );
111
+ const [, e, t, a, c, n, o] = i, s = d.model.nodes.get(t);
112
+ if (!s) throw new Error(`${t} is not a @node type`);
113
+ const r = o ? JSON.parse(o) : {}, u = `${s.key.customScalar ?? s.key.type}!`, p = t.charAt(0).toLowerCase() + t.slice(1), $ = s.plural.charAt(0).toUpperCase() + s.plural.slice(1), m = ["connect", "disconnect", "update-edge"].includes(
114
+ e
115
+ );
116
+ if (m !== (a !== void 0 && n !== void 0))
117
+ throw new Error(
118
+ m ? `${e} takes Type.field key → key` : `${e} takes Type key`
119
+ );
120
+ switch (e) {
121
+ case "read":
122
+ return {
123
+ operation: e,
124
+ source: `query($key: ${u}) { ${p}(${s.key.name}: $key) { ${s.key.name} } }`,
125
+ variables: { key: c },
126
+ isRead: !0,
127
+ root: p
128
+ };
129
+ case "create":
130
+ return {
131
+ operation: e,
132
+ source: `mutation($input: [${t}CreateInput!]!) { create${$}(input: $input) { __typename } }`,
133
+ variables: { input: [{ ...r, [s.key.name]: c }] },
134
+ isRead: !1,
135
+ root: `create${$}`
136
+ };
137
+ case "update":
138
+ return {
139
+ operation: e,
140
+ source: `mutation($key: ${u}, $update: ${t}UpdateInput!) { update${t}(${s.key.name}: $key, update: $update) { __typename } }`,
141
+ variables: { key: c, update: r },
142
+ isRead: !1,
143
+ root: `update${t}`
144
+ };
145
+ case "delete":
146
+ return {
147
+ operation: e,
148
+ source: `mutation($key: ${u}) { delete${t}(${s.key.name}: $key) { __typename } }`,
149
+ variables: { key: c },
150
+ isRead: !1,
151
+ root: `delete${t}`
152
+ };
153
+ }
154
+ const f = s.fields.get(a);
155
+ if (f?.kind !== "relationship")
156
+ throw new Error(`${t}.${a} is not a relationship field`);
157
+ const y = d.model.nodes.get(f.target)?.key.name ?? "key", k = e === "connect" ? {
158
+ connect: f.list ? [{ [y]: n, ...o ? { edge: r } : {} }] : { [y]: n, ...o ? { edge: r } : {} }
159
+ } : e === "disconnect" ? { disconnect: f.list ? [n] : !0 } : { update: [{ [y]: n, edge: r }] };
160
+ return {
161
+ operation: e,
162
+ source: `mutation($key: ${u}, $update: ${t}UpdateInput!) { update${t}(${s.key.name}: $key, update: $update) { __typename } }`,
163
+ variables: { key: c, update: { [a]: k } },
164
+ isRead: !1,
165
+ root: `update${t}`
166
+ };
167
+ }
52
168
  export {
53
- L as createTestLoraGraphQL,
54
- b as expectSeeks
169
+ _ as createTestLoraGraphQL,
170
+ x as expectAccess,
171
+ N as expectSeeks
55
172
  };
56
173
  //# sourceMappingURL=testing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","sources":["../src/testing.ts"],"sourcesContent":["// Test helpers: an in-memory LoraDB with the schema asserted, and a plan\n// assertion. Import from `@loradb/lora-graphql/testing`; needs\n// `@loradb/lora-node`. Framework-agnostic: failures throw plain errors.\n\nimport {\n graphql,\n type DocumentNode,\n type ExecutionResult,\n type GraphQLSchema,\n} from \"graphql\";\nimport { loraDriver, type LoraDatabaseLike } from \"./driver.js\";\nimport {\n LoraGraphQL,\n type LoraGraphQLOptions,\n type StatementEvent,\n} from \"./lora-graphql.js\";\n\n/** The node binding's database, as far as tests use it. */\nexport type TestDatabase = LoraDatabaseLike & {\n execute(query: string, params?: never): Promise<unknown>;\n dispose?(): void;\n};\n\nexport interface TestLoraGraphQLOptions extends Omit<\n LoraGraphQLOptions,\n \"driver\"\n> {\n /** Cypher to run after the schema is asserted, or a function given the database. */\n seed?: string | string[] | ((db: TestDatabase) => Promise<void>);\n}\n\nexport interface TestLoraGraphQL {\n lora: LoraGraphQL;\n db: TestDatabase;\n schema: GraphQLSchema;\n /** Every statement run so far (clear with `statements.length = 0`). */\n statements: StatementEvent[];\n /** Execute a document; errors stay in the result. */\n run: (\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => Promise<ExecutionResult>;\n /** Execute a document and return its data; throws on the first error. */\n data: <T = Record<string, unknown>>(\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => Promise<T>;\n /** Release the database. */\n close: () => void;\n}\n\n/** A LoraGraphQL over a fresh in-memory database, schema asserted and seeded. */\nexport async function createTestLoraGraphQL(\n options: TestLoraGraphQLOptions,\n): Promise<TestLoraGraphQL> {\n let createDatabase: () => Promise<TestDatabase>;\n try {\n ({ createDatabase } = (await import(\"@loradb/lora-node\")) as unknown as {\n createDatabase: typeof createDatabase;\n });\n } catch {\n throw new Error(\n \"createTestLoraGraphQL needs @loradb/lora-node: npm install --save-dev @loradb/lora-node\",\n );\n }\n const db = await createDatabase();\n const statements: StatementEvent[] = [];\n const { seed, onStatement, ...rest } = options;\n const lora = new LoraGraphQL({\n ...rest,\n driver: loraDriver(db),\n onStatement: (event) => {\n statements.push(event);\n onStatement?.(event);\n },\n });\n await lora.assertSchema({ create: true });\n if (typeof seed === \"function\") await seed(db);\n else {\n for (const text of typeof seed === \"string\" ? [seed] : (seed ?? [])) {\n await db.execute(text);\n }\n }\n const schema = lora.getSchema();\n const run = (\n source: string,\n variables: Record<string, unknown> = {},\n contextValue: Record<string, unknown> = {},\n ) => graphql({ schema, source, variableValues: variables, contextValue });\n return {\n lora,\n db,\n schema,\n statements,\n run,\n data: async <T>(\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => {\n const r = await run(source, variables, context);\n if (r.errors) throw r.errors[0];\n return r.data as T;\n },\n close: () => db.dispose?.(),\n };\n}\n\n/**\n * Assert that every statement of a query's root fields uses the index\n * access it was compiled for (no label scans where a seek was expected,\n * no mutating plans, no row-budget excess). Throws listing each finding.\n */\nexport async function expectSeeks(\n lora: LoraGraphQL,\n document: string | DocumentNode,\n variables: Record<string, unknown> = {},\n options: { rowBudget?: number; context?: unknown } = {},\n): Promise<void> {\n const fields = await lora.explain(document, variables, options);\n const findings = fields.flatMap((f) =>\n f.reports.flatMap((r) =>\n r.findings.map((x) => `${f.field}: ${x.message}\\n ${x.statement}`),\n ),\n );\n if (findings.length > 0) {\n throw new Error(\n `expected every root field to seek:\\n${findings.join(\"\\n\")}`,\n );\n }\n}\n"],"names":["createTestLoraGraphQL","options","createDatabase","db","statements","seed","onStatement","rest","lora","LoraGraphQL","loraDriver","event","text","schema","run","source","variables","contextValue","graphql","context","r","expectSeeks","document","findings","f","x"],"mappings":";;AAsDA,eAAsBA,EACpBC,GAC0B;AAC1B,MAAIC;AACJ,MAAI;AACF,KAAC,EAAE,gBAAAA,EAAA,IAAoB,MAAM,OAAO,mBAAmB;AAAA,EAGzD,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,QAAMC,IAAK,MAAMD,EAAA,GACXE,IAA+B,CAAA,GAC/B,EAAE,MAAAC,GAAM,aAAAC,GAAa,GAAGC,MAASN,GACjCO,IAAO,IAAIC,EAAY;AAAA,IAC3B,GAAGF;AAAA,IACH,QAAQG,EAAWP,CAAE;AAAA,IACrB,aAAa,CAACQ,MAAU;AACtB,MAAAP,EAAW,KAAKO,CAAK,GACrBL,IAAcK,CAAK;AAAA,IACrB;AAAA,EAAA,CACD;AAED,MADA,MAAMH,EAAK,aAAa,EAAE,QAAQ,IAAM,GACpC,OAAOH,KAAS,WAAY,OAAMA,EAAKF,CAAE;AAAA;AAE3C,eAAWS,KAAQ,OAAOP,KAAS,WAAW,CAACA,CAAI,IAAKA,KAAQ;AAC9D,YAAMF,EAAG,QAAQS,CAAI;AAGzB,QAAMC,IAASL,EAAK,UAAA,GACdM,IAAM,CACVC,GACAC,IAAqC,CAAA,GACrCC,IAAwC,CAAA,MACrCC,EAAQ,EAAE,QAAAL,GAAQ,QAAAE,GAAQ,gBAAgBC,GAAW,cAAAC,GAAc;AACxE,SAAO;AAAA,IACL,MAAAT;AAAA,IACA,IAAAL;AAAA,IACA,QAAAU;AAAA,IACA,YAAAT;AAAA,IACA,KAAAU;AAAA,IACA,MAAM,OACJC,GACAC,GACAG,MACG;AACH,YAAMC,IAAI,MAAMN,EAAIC,GAAQC,GAAWG,CAAO;AAC9C,UAAIC,EAAE,OAAQ,OAAMA,EAAE,OAAO,CAAC;AAC9B,aAAOA,EAAE;AAAA,IACX;AAAA,IACA,OAAO,MAAMjB,EAAG,UAAA;AAAA,EAAU;AAE9B;AAOA,eAAsBkB,EACpBb,GACAc,GACAN,IAAqC,CAAA,GACrCf,IAAqD,IACtC;AAEf,QAAMsB,KADS,MAAMf,EAAK,QAAQc,GAAUN,GAAWf,CAAO,GACtC;AAAA,IAAQ,CAACuB,MAC/BA,EAAE,QAAQ;AAAA,MAAQ,CAAC,MACjB,EAAE,SAAS,IAAI,CAACC,MAAM,GAAGD,EAAE,KAAK,KAAKC,EAAE,OAAO;AAAA,IAAOA,EAAE,SAAS,EAAE;AAAA,IAAA;AAAA,EACpE;AAEF,MAAIF,EAAS,SAAS;AACpB,UAAM,IAAI;AAAA,MACR;AAAA,EAAuCA,EAAS,KAAK;AAAA,CAAI,CAAC;AAAA,IAAA;AAGhE;"}
1
+ {"version":3,"file":"testing.js","sources":["../src/testing.ts"],"sourcesContent":["// Test helpers: an in-memory LoraDB with the schema asserted, and a plan\n// assertion. Import from `@loradb/lora-graphql/testing`; needs\n// `@loradb/lora-node`. Framework-agnostic: failures throw plain errors.\n\nimport {\n graphql,\n type DocumentNode,\n type ExecutionResult,\n type GraphQLSchema,\n} from \"graphql\";\nimport { loraDriver, type LoraDatabaseLike } from \"./driver.js\";\nimport {\n LoraGraphQL,\n type LoraGraphQLOptions,\n type StatementEvent,\n} from \"./lora-graphql.js\";\n\n/** The node binding's database, as far as tests use it. */\nexport type TestDatabase = LoraDatabaseLike & {\n execute(query: string, params?: never): Promise<unknown>;\n dispose?(): void;\n};\n\nexport interface TestLoraGraphQLOptions extends Omit<\n LoraGraphQLOptions,\n \"driver\"\n> {\n /** Cypher to run after the schema is asserted, or a function given the database. */\n seed?: string | string[] | ((db: TestDatabase) => Promise<void>);\n}\n\nexport interface TestLoraGraphQL {\n lora: LoraGraphQL;\n db: TestDatabase;\n schema: GraphQLSchema;\n /** Every statement run so far (clear with `statements.length = 0`). */\n statements: StatementEvent[];\n /** Execute a document; errors stay in the result. */\n run: (\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => Promise<ExecutionResult>;\n /** Execute a document and return its data; throws on the first error. */\n data: <T = Record<string, unknown>>(\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => Promise<T>;\n /** Release the database. */\n close: () => void;\n}\n\n/** A LoraGraphQL over a fresh in-memory database, schema asserted and seeded. */\nexport async function createTestLoraGraphQL(\n options: TestLoraGraphQLOptions,\n): Promise<TestLoraGraphQL> {\n let createDatabase: () => Promise<TestDatabase>;\n try {\n ({ createDatabase } = (await import(\"@loradb/lora-node\")) as unknown as {\n createDatabase: typeof createDatabase;\n });\n } catch {\n throw new Error(\n \"createTestLoraGraphQL needs @loradb/lora-node: npm install --save-dev @loradb/lora-node\",\n );\n }\n const db = await createDatabase();\n const statements: StatementEvent[] = [];\n const { seed, onStatement, ...rest } = options;\n const lora = new LoraGraphQL({\n ...rest,\n driver: loraDriver(db),\n onStatement: (event) => {\n statements.push(event);\n onStatement?.(event);\n },\n });\n await lora.assertSchema({ create: true });\n if (typeof seed === \"function\") await seed(db);\n else {\n for (const text of typeof seed === \"string\" ? [seed] : (seed ?? [])) {\n await db.execute(text);\n }\n }\n const schema = lora.getSchema();\n const run = (\n source: string,\n variables: Record<string, unknown> = {},\n contextValue: Record<string, unknown> = {},\n ) => graphql({ schema, source, variableValues: variables, contextValue });\n return {\n lora,\n db,\n schema,\n statements,\n run,\n data: async <T>(\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => {\n const r = await run(source, variables, context);\n if (r.errors) throw r.errors[0];\n return r.data as T;\n },\n close: () => db.dispose?.(),\n };\n}\n\n/**\n * Assert that every statement of a query's root fields uses the index\n * access it was compiled for (no label scans where a seek was expected,\n * no mutating plans, no row-budget excess). Throws listing each finding.\n */\nexport async function expectSeeks(\n lora: LoraGraphQL,\n document: string | DocumentNode,\n variables: Record<string, unknown> = {},\n options: { rowBudget?: number; context?: unknown } = {},\n): Promise<void> {\n const fields = await lora.explain(document, variables, options);\n const findings = fields.flatMap((f) =>\n f.reports.flatMap((r) =>\n r.findings.map((x) => `${f.field}: ${x.message}\\n ${x.statement}`),\n ),\n );\n if (findings.length > 0) {\n throw new Error(\n `expected every root field to seek:\\n${findings.join(\"\\n\")}`,\n );\n }\n}\n\n/** The access `expectAccess` expects: entries allowed, and entries denied. */\nexport interface AccessExpectations {\n /** The claims to act as; undefined acts anonymously. */\n as: Record<string, unknown> | undefined;\n /** Other GraphQL context values the rules read (`$context.*`). */\n context?: Record<string, unknown>;\n allowed?: readonly string[];\n denied?: readonly string[];\n}\n\n/**\n * Assert who may do what, against the database: each entry runs as the\n * caller in a transaction that is always rolled back, and every mismatch\n * is reported at once. Entries:\n *\n * - `read Trip lou:tomorrowland` (denied: not visible, or refused)\n * - `create Trip lou:x {\"name\": \"X\"}` (other input fields as JSON)\n * - `update Trip lou:x {\"name\": \"Y\"}` and `delete Trip lou:x`\n * - `connect Trip.members lou:tomorrowland → f1` (also `->`)\n * - `disconnect Trip.members lou:tomorrowland → f1`\n * - `update-edge Trip.members lou:tomorrowland → f1 {\"rsvp\": \"GOING\"}`\n *\n * Denied means `FORBIDDEN`, `UNAUTHENTICATED` or `NOT_FOUND` (a node the\n * caller cannot see); any other error is reported as a mismatch either way.\n */\nexport async function expectAccess(\n target: LoraGraphQL | { lora: LoraGraphQL },\n expectations: AccessExpectations,\n): Promise<void> {\n const lora = target instanceof LoraGraphQL ? target : target.lora;\n const problems: string[] = [];\n const check = async (entry: string, allowed: boolean) => {\n let outcome: AccessOutcome;\n try {\n outcome = await tryAccess(lora, entry, expectations);\n } catch (err) {\n problems.push(\n `${entry}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return;\n }\n if (outcome.kind === \"error\") {\n problems.push(`${entry}: failed with ${outcome.message}`);\n } else if (allowed && outcome.kind === \"denied\") {\n problems.push(\n `expected allowed, was denied: ${entry} (${outcome.message})`,\n );\n } else if (!allowed && outcome.kind === \"allowed\") {\n problems.push(`expected denied, was allowed: ${entry}`);\n }\n };\n for (const entry of expectations.allowed ?? []) await check(entry, true);\n for (const entry of expectations.denied ?? []) await check(entry, false);\n if (problems.length > 0) {\n const who = expectations.as ? JSON.stringify(expectations.as) : \"anonymous\";\n throw new Error(\n `access as ${who} differs in ${problems.length} ${problems.length === 1 ? \"entry\" : \"entries\"}:\\n ${problems.join(\"\\n \")}`,\n );\n }\n}\n\ntype AccessOutcome =\n | { kind: \"allowed\" }\n | { kind: \"denied\"; message: string }\n | { kind: \"error\"; message: string };\n\nconst DENIED = new Set([\"FORBIDDEN\", \"UNAUTHENTICATED\", \"NOT_FOUND\"]);\n\nasync function tryAccess(\n lora: LoraGraphQL,\n entry: string,\n expectations: AccessExpectations,\n): Promise<AccessOutcome> {\n const { source, variables, isRead, root } = accessOperation(lora, entry);\n const tx = await lora.begin();\n try {\n const result = await lora.execute({\n source,\n variables,\n context: {\n ...expectations.context,\n ...(expectations.as ? { jwt: expectations.as } : {}),\n transaction: tx,\n },\n });\n const error = result.errors?.[0];\n if (error) {\n const code = (error.extensions as Record<string, unknown> | undefined)?.[\n \"code\"\n ];\n return typeof code === \"string\" && DENIED.has(code)\n ? { kind: \"denied\", message: `${code}: ${error.message}` }\n : {\n kind: \"error\",\n message: `${String(code ?? \"error\")}: ${error.message}`,\n };\n }\n if (isRead) {\n const value = (result.data as Record<string, unknown> | null)?.[root];\n if (value == null) return { kind: \"denied\", message: \"not visible\" };\n }\n return { kind: \"allowed\" };\n } finally {\n await tx.rollback();\n }\n}\n\nfunction accessOperation(\n lora: LoraGraphQL,\n entry: string,\n): {\n operation: string;\n source: string;\n variables: Record<string, unknown>;\n isRead: boolean;\n root: string;\n} {\n const m =\n /^\\s*(read|create|update|delete|connect|disconnect|update-edge)\\s+([A-Za-z_][\\w]*)(?:\\.([A-Za-z_]\\w*))?\\s+(\\S+)(?:\\s*(?:→|->)\\s*(\\S+))?\\s*(\\{.*\\})?\\s*$/s.exec(\n entry,\n );\n if (!m) {\n throw new Error(\n \"expected `<read|create|update|delete> Type key [json]` or `<connect|disconnect|update-edge> Type.field key → key [json]`\",\n );\n }\n const [, operation, typeName, fieldName, key, other, json] = m as unknown as [\n string,\n string,\n string,\n string | undefined,\n string,\n string | undefined,\n string | undefined,\n ];\n const node = lora.model.nodes.get(typeName);\n if (!node) throw new Error(`${typeName} is not a @node type`);\n const input = json ? (JSON.parse(json) as Record<string, unknown>) : {};\n const keyType = `${node.key.customScalar ?? node.key.type}!`;\n const lower = typeName.charAt(0).toLowerCase() + typeName.slice(1);\n const upperPlural =\n node.plural.charAt(0).toUpperCase() + node.plural.slice(1);\n const relational = [\"connect\", \"disconnect\", \"update-edge\"].includes(\n operation,\n );\n if (relational !== (fieldName !== undefined && other !== undefined)) {\n throw new Error(\n relational\n ? `${operation} takes Type.field key → key`\n : `${operation} takes Type key`,\n );\n }\n switch (operation) {\n case \"read\":\n return {\n operation,\n source: `query($key: ${keyType}) { ${lower}(${node.key.name}: $key) { ${node.key.name} } }`,\n variables: { key },\n isRead: true,\n root: lower,\n };\n case \"create\":\n return {\n operation,\n source: `mutation($input: [${typeName}CreateInput!]!) { create${upperPlural}(input: $input) { __typename } }`,\n variables: { input: [{ ...input, [node.key.name]: key }] },\n isRead: false,\n root: `create${upperPlural}`,\n };\n case \"update\":\n return {\n operation,\n source: `mutation($key: ${keyType}, $update: ${typeName}UpdateInput!) { update${typeName}(${node.key.name}: $key, update: $update) { __typename } }`,\n variables: { key, update: input },\n isRead: false,\n root: `update${typeName}`,\n };\n case \"delete\":\n return {\n operation,\n source: `mutation($key: ${keyType}) { delete${typeName}(${node.key.name}: $key) { __typename } }`,\n variables: { key },\n isRead: false,\n root: `delete${typeName}`,\n };\n }\n const field = node.fields.get(fieldName!);\n if (field?.kind !== \"relationship\") {\n throw new Error(`${typeName}.${fieldName} is not a relationship field`);\n }\n const targetKey = lora.model.nodes.get(field.target)?.key.name ?? \"key\";\n const change =\n operation === \"connect\"\n ? {\n connect: field.list\n ? [{ [targetKey]: other, ...(json ? { edge: input } : {}) }]\n : { [targetKey]: other, ...(json ? { edge: input } : {}) },\n }\n : operation === \"disconnect\"\n ? { disconnect: field.list ? [other] : true }\n : { update: [{ [targetKey]: other, edge: input }] };\n return {\n operation,\n source: `mutation($key: ${keyType}, $update: ${typeName}UpdateInput!) { update${typeName}(${node.key.name}: $key, update: $update) { __typename } }`,\n variables: { key, update: { [fieldName!]: change } },\n isRead: false,\n root: `update${typeName}`,\n };\n}\n"],"names":["createTestLoraGraphQL","options","createDatabase","db","statements","seed","onStatement","rest","lora","LoraGraphQL","loraDriver","event","text","schema","run","source","variables","contextValue","graphql","context","r","expectSeeks","document","findings","f","x","expectAccess","target","expectations","problems","check","entry","allowed","outcome","tryAccess","err","who","DENIED","isRead","root","accessOperation","tx","result","error","code","m","operation","typeName","fieldName","key","other","json","node","input","keyType","lower","upperPlural","relational","field","targetKey","change"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loradb/lora-graphql",
3
- "version": "0.19.0",
3
+ "version": "0.20.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.19.0",
68
+ "@loradb/lora-node": "^0.20.0",
69
69
  "graphql": "^16.8.0 || ^17.0.0"
70
70
  },
71
71
  "devDependencies": {